C/C++中的sequence point和side effect

来源是在网上看到的一段代码,用于交换两个变量值:

01 #include <stdio.h>
02
03
/*
04
Why the first swap operation works as expected but
05
it does not happen the same with the second one ?
06
I guess that it can be due operations within temp values,
07
but IMHO swap operation should work in both cases.
08
09
Thanks !
10
*/
11
12
void swap(int *a, int *b) {
13
*a ^= *b ^= *a ^= *b;
14
}
15

16
int main() {
17
int a = 5;
18
int b = 8;
19
printf(“%d, %d\n, a, b);
20
a ^= b ^= a ^= b;
21
printf(“%d, %d\n, a, b);
22
swap(&a, &b);
23
printf(“%d, %d\n, a, b);
24
}

经GCC编译后,代码运行的结果是:
5,8
8,5
0,8

理由是:编译器在翻译第十三行的时候,没能给出正确的解释(可能增加了临时变量之类的,具体的讨论见这里)。
为什么对于第13行和第20行编译器会给出不同的解释?这个就和顺序点(sequence point)副作用(side effect)有关。我在网上找到了一些关于这两个概念的解释,觉得下面两个稍微容易懂些:

副作用(side effect):

表达式有两种功能:每个表达式都产生一个值( value ),同时可能包含副作用( side effect )。副作用是指改变了某些变量的值。
如:
1:   20         //这个表达式的值是20;它没有副作用,因为它没有改变任何变量的值。
2:   x=5       // 这个表达式的值是5;它有一个副作用,因为它改变了变量x的值。
3:   x=y++     // 这个表达示有两个副作用,因为改变了两个变量的值。
4:   x=x++     // 这个表达式也有两个副作用,因为变量x的值发生了两次改变。

顺序点(sequence point):

顺序点的意思是在一系列步骤中的一个“结算”的点,语言要求这一时刻的求值和副作用全部完成,才能进入下面的部分。在C/C++中只有以下几种存在顺序点:
1)  分号;
2)  未重载的逗号运算符的左操作数赋值之后(即’,'处)
3)  未重载的’||’运算符的左操作数赋值之后(即’||’处);
4)  未重载的’&&’运算符的左操作数赋值之后(即”&&”处);
5)  三元运算符’? : ‘的左操作数赋值之后(即’?'处);
6)  在函数所有参数赋值之后但在函数第一条语句执行之前;
7)  在函数返回值已拷贝给调用者之后但在该函数之外的代码执行之前;
8)  每个基类和成员初始化之后;
9)  在每一个完整的变量声明处有一个顺序点,例如int i, j;中逗号和分号处分别有一个顺序点;
10) for循环控制条件中的两个分号处各有一个顺序点。

C++标准中规定:An expression can modify an object’s value only once between consecutive “sequence points.” (在两个顺序点之间,一个值只能被赋值一次)如果违背了这个规定,那么这两个顺序点之间产生的任何副作用的发生顺序都是未定义的(不被标准限制,依赖于编译器的实现)。

维基百科上有两个例子:
f() + g()  这里只有一个顺序点“;”,函数 f 和函数 g 的执行顺序是undefined的,在不同厂商的编译器中,f 和g 都有可能先执行。
i = i++     这个例子在一个顺序点之前,对 i 进行了两次修改,违背了上面的规定。因此最后 i 的值也是不确定的。

另外,我还在网上看到了一个说法:标准还规定,如果表达式求值过程会更改某对象的值,那么要求更改前的值被读取的唯一目的,只能是用来确定要存入的新值。(通俗点,也就是如果某个对象会被修改,同时这个对象会被读取用作其他用途,那么就违背了规定。后果就是,读取和修改的发生顺序会被不同的编译器给出不同的解释) 对于这种说法,有两个例子:
x[i] = i++ ,在这个顺序点之前,i 虽然只被修改了一次,但是 i 被读取不是为了确定 i 的新值,而是为了确定数组中被修改元素的下标,这也违背了规定。
function(x, x++) 这里只有一个顺序点,x 只被修改了一次,但是 x 同时被读取作为函数参数,违背了规定,那么 x 就有可能先被加1,再被读取,也有可能先被读取,再被加1。

总之,尽量保证不违反这两个规定,在两个相邻顺序点之间同一个变量不可以被修改两次以上或者同时有读取和修改,否则,就会产生未定义的行为。非要仔细研究这个规则意义不大,写代码的时候写得清晰一点,不要故意炫耀技巧就可以了。

2012年2月22日 | 归档于 C/C++, Computer Science
标签: ,

linux下安装jsoncpp

项目里面需要用到json,于是下了源码。但不想把去编译它的源码(这样会对我的Makefile改动较大),于是打算直接装一个静态库,然后引用头文件即可。

了解到想安装json,必须有scons,于是下了scons的源码,然后对着它的README研究了半天,同时还对照着jsoncpp的README研究。在执行 python scons.py platform=PLTFRM [TARGET] 总是不对(同时,在我所下载的包里面,是没有scons.py文件的,只有一个scons文件),提示缺少某种module。

又研究了半天,最后还是在网上找到了正确的方法。

1.下载scons (我的是scons-2.1.0.tar.gz)(scons需要python)

2.下载jsoncpp (我的是jsoncpp-src-0.5.0.tar.gz

3. 解压缩scons

       tar –xvf  scons-2.1.0.tar.gz

4. 设置环境变量 (这步是关键)

       export MYSCONS = 解压的路径

       export SCONS_LIB_DIR = $MYSCONS/engine

5. 解压缩jsoncpp,并进入json目录

     tar –xvf  jsoncpp-src-0.5.0.tar.gz

6. 执行命令

      python $MYSCONS/script/scons platform=linux-gcc

7. 在jsoncpp-src-0.5.0/libs/linux-gcc-4.1.2目录下,生成了两个库文件,你可以改成短一点的名字

      libjson_linux-gcc-4.4.4_libmt.a

      libjson_linux-gcc-4.4.4_libmt.so

8. 将这两个文件拷贝到你项目的目录就可以了~

2011年12月22日 | 归档于 Computer Science, Linux

计算机字符编码

转载自:bigwhite.blogbus.com/logs/10617585.html

字符,这个我们在平时编码过程中最最常见的元素,其实也有着一段小故事。

计算机,毫无疑问是一部机器,在最初我们接触计算机时或者接收计算机教育时,我们就知道:计算机能识别的只有010101的二进制码。人与计算机交互早期也是用的是二进制方式,当时人们或通过扳动计算机庞大的面板上无数的开关来向计算机输入信息,或使用打孔卡片来向计算机输入指令和数据。终端和键盘组成的字符人机界面的诞生让人们大大提高了与计算机的交互效率。这里提到了’字符’,那么什么是’字符’?说的通俗些:字符就是人们使用的记号,抽象意义上的一个符号。比如阿拉伯数字1,这就是一个符号,这个符号的抽象含义:1代表一种数量的概念,关于1这个抽象概念是如何诞生的,有兴趣的人可以去翻阅一下类似数学史之类科普书籍。

人类的记号五花八门,包括国家文字、标点符号、图形符号、数字等。这些在计算机领域会被统称为’字符’。而所有字符的集合就被称为’字符集’。有了’字符’概念,那么在计算机中如何表示’字符’呢?前文提到了计算机中都是用二进制bit来交流的,’字符’也只能建筑在bit的基础上。多少bit表示一个字符合适呢?或者说我们的字符集有多大呢?如果字符集里只有8个字符,那么我用3个bit的组合就可以将这些字符都表示和识别出来。想当年美国人也在考虑这个问题,不过美国人想当然的就认为:所有能用到的有现实意义的字符不超过256个,当时美国人也只用到了128个,预留128个备用,而256个字符的字符集用8bit就可以表示,这就是举世闻名的美国标准信息交换代码( American Standard Code for Information Interchange, ASCII)。而这8bit恰与计算机中的基本存储数据单元-’字节’的位个数相同,这样一个字节就恰可以表示一个ASCII字符了。如:ASCII字符 ‘A’的内存位模式:0×41。

这里提到了一个’编码’的概念,上面提到的ASCII就是众多字符编码规范中的一种,最早的一种,最重要的一种。那么什么是字符编码呢?回顾一下ASCII在制订的时候都做了哪些事:

1)  规定用8bit即一个字节来表示一个ASCII字符;
2)  制定了ASCII字符表,即该字符集中的每个字符对应的位模式。如:ASCII字符’B'的内存位模式:0×42,’1′的内存位模式:0×31。

由此看来一个字符编码规范要做两件事:
1)  规定这个字符集中的字符用多少字节来表示;
2)  制订该字符编码集的字符表,即该字符集中每个字符对应的位模式。

1)和2)这两个规定合在一起就是编码。

随着计算机的普及,世界各国都开始使用计算机,但是对于非英语国家如中、日、韩等来说,ASCII码是远远不能满足本国人的需要的,我中华文明渊源五千年,这五千年来积淀下来的文明怎是这256个字符(精确的说是128个字符)所能表达出来的。我们也要制定自己的编码,同样日本人、韩国人也都是这么做的。这样一来,世界范围内就多了诸如GB2312、BIG5、JIS等局限于某个国家或地区使用的本地化编码标准,这些编码标准被统称为:ANSI编码。这些ANSI编码有一些共同的特点:

1)  每种ANSI编码或者说ANSI字符集只规定自己国家或地区使用的语言所需的’字符’;比如中文GB-2312编码中就不会包含韩国人的文字。
2)  ANSI字符集的空间都比ASCII要大很多,一个字节已经不够,绝大多数都使用了多字节的存储方案。
3)  ANSI编码一般都会兼容ASCII码。

ANSI 的出现让计算机迅速普及到世界的每个角落,每个国家都利用上了这样的先进的工具提高了自己的生产力。打开Windows记事本,”另存为”对话框的”编码”下拉框中有ANSI编码,在简体中文系统下,ANSI编码代表GB2312编码,在日文操作系统下,ANSI 编码代表 JIS 编码。但是随着互联网的兴起,问题出现了。由于ANSI码的第一个特点:各个国家或地区在编制自己的ANSI码时并未考虑到其他国家或地区的ANSI码,导致编码空间有重叠,比如:汉字’中’的GB编码是[0xD6,0xD0],这个编码在JIS中是什么呢,我不知道,我也不愿意去查那些稀奇古怪的鬼子文,但我可以肯定的是肯定不是’中’这个字符了,虽然鬼子的语言文字中抄袭了大量的汉文字。这样一来当在不同ANSI编码系统之间进行信息交换和展示的时候,乱码就不可避免了。

为了使国际间信息交流更加方便,Unicode字符集编码诞生。Unicode是Universal Multiple-Octet Coded Character Set的缩写,中文含义是”通用多八位编码字符集”。它是由一个名为 Unicode学术学会(Unicode Consortium)的机构制订的字符编码系统,Unicode目标是将世界上绝大多数国家和的确的文字、符号都编入其字符集,它为每种语言中的每个字符设定了统一并且唯一的二进制编码(位模式),以满足跨语言、跨平台进行文本转换、处理的要求,以达到支持现今世界各种不同语言的书面文本的交换、处理及显示的目的,使世界范围人们通过计算机进行信息交换时达到畅通自如而无障碍。说白了Unicode编码就是先将世界上存在的绝大多数常用字符纳入 Unicode字符集,然后进行统一排号。而每个Unicode字符的编码(位模式)就是该字符在Unicode字符表中的序号,所以与上面提到的 ANSI编码不同的是,一个Unicode字符的编码用的是一个整数表示,而这个整数的长度通常>= 2个字节。这样Unicode编码在不同平台存储时就要注意其字节序了。比如:采用标准Unicode编码的’中’在Windows上的存储就是 ’2D4E’,而在SPARC Solaris上的存储则是’4E2D’。

上面提到了标准Unicode编码,难道还有其他Unicode编码方式,的确,Unicode的出现的确使我们在统一计算机编码过程中迈出的一大步,但是毕竟Unicode诞生才10几年,这之前大家一直使用ASCII码,一直使用各自的ANSI编码。要想一次性将全世界的计算机系统都统一改为 Unicode编码,可能性不大。那么现在越来越多的新系统都开始支持并使用Unicode,这些新系统与旧系统之间如何交换数据其实是首要难题。于是一个新名词又诞生了,那就是UTF, Unicode Translation Format,即把Unicode转做某种格式的意思。为什么要转换成某种格式呢?转换是为了传输和交换。一种好的UTF-x方案应该便于在不同的计算机之间使用网络传输不同语言和编码的文字,使得标准双字节的Unicode能够在现存的处理单字节的系统上正确传输。目前比较常见的UTF方案有三种:

UTF-16:其本身就是标准的Unicode编码方案,又称为UCS-2,它固定使用16 bits(两个字节)整数来表示一个字符。
UTF-32:又称为UCS-4,它固定使用32 bits(四个字节)整数来表示一个字符。
UTF- 8:最广泛的使用的UTF方案,UTF-8使用可变长度字节来储存Unicode字符,例如ASCII字母继续使用1字节储存,重音文字、希腊字母或西里尔字母等使用2字节来储存,而常用的汉字就要使用3字节。辅助平面字符则使用4字节。UTF-8更便于在使用Unicode的系统与现存的单字节的系统进行数据传输和交换。与前两个方案不同:UTF-8以字节为编码单元,没有字节序的问题。

UTF有三种方案,那么如何在接收数据和存储数据时识别数据和指导识别数据采用的是哪个方案呢?在UTF编码方案中有一个叫做”ZERO WIDTH NO-BREAK SPACE”的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输或存储中。UCS规范建议我们在传输或存储字节流前,先传输字符”ZERO WIDTH NO-BREAK SPACE”。这样根据识别前面的”ZERO WIDTH NO-BREAK SPACE”即可识别编码方案:
EF BB BF UTF-8
FE FF UTF-16/UCS-2, little endian
FF FE UTF-16/UCS-2, big endian
FF FE 00 00 UTF-32/UCS-4, little endian.
00 00 FE FF UTF-32/UCS-4, big-endian.

以上是简略的字符编码的基本知识。下面将编码与具体的编程语言结合起来进行更直观的学习。这里还是以C语言举例。

C 语言定义了两个字符集(character set):源代码字符集(source character set)是用于组成C源代码的字符集合,而运行字符集(execution character set)是可以被执行程序解释的字符集合。应用程序都有自己的执行字符集,也就说在应用程序执行过程中使用什么字符集或字符编码来识别各种数据存储介质中的bit流。

[Example1]
/* testwprintf.c , windows xp, mingw gcc-3.4.2 */
int main()
{
wchar_t ws[] = L”中文”; — (1)
wprintf(L”%s\n”, ws);
}

编译该程序gcc编译器提示:(1)这行:converting to execution character set: Illegal byte sequence
为什么转换失败呢?我们看到程序中使用了宽字符常量。这里先插入一段C语言的小故事:多字节字符和宽字节字符。

C 语言原本是在英文环境中设计的,主要的字符集是ASCII字符。但是国际化软件必须能够表示不同的字符,而这些字符数量庞大,无法使用一个字节编码,于是在1994年,”Normative Addendum 1″(基准增补一)的采用,让ISO C可以标准化两种表示大型字符集的方法:宽字符(wide character,该字符集内每个字符使用相同的位长)以及多字节字符(multibyte character,每个字符可以是一到多个字节不等,而某个字节序列的字符值由字符串或流(stream)所在的环境背景决定)。自从1994 年的增补之后,C不只提供char类型,还提供wchar_t类型(宽字符)。虽然此次C标准仍没有支持Unicode字符集,但许多实现版本使用 Unicode转换格式UTF-16和UTF-32来处理宽字符(我遇到的mingw gcc用的是UTF-16, Sun Sparc Gcc用的则是UTF-32),也就是说在大部分标准C实现版本中,默认的一个wchar_t就是一个unicode字符,一个宽字符实际上就是一个 unicode字符,一个宽字符常量字符串(L”…”)实际上是一个unicode编码的常量字符串。这样我们来解释上面的问题。

上面程序中编译器在遇到宽字符常量:L”中文”时,试图将之转换成unicode码存储,mingw gcc试图使用默认的源代码符号集->unicode的转码方式转换”中文”这个字面量的二进制位模式到unicode位模式,但却发现”中文”这个字面量的位模式不能识别,这就需要我们在外部告知gcc我们的这个”中文”字面量的位模式是GB2312的,我们使用:gcc -finput-charset=GB2312 testwprintf.c就能解决这一问题了。

好了,编译完了。我们来执行一下 a.exe,但却发现在控制台没有任何输出,又出现什么问题了呢?分析一下:目前我们的ws中使用的位模式是unicode编码位模式,哇,原来 wprintf并不支持直接输出:unicode编码。类似:printf, wprintf等输出到控制台或者文件的库函数只支持ANSI编码或多字节编码输出。其实这是符合C语言规范的,因为C标准并未支持Unicode,只是很多C的实现将宽字符用unicode的位模式表示罢了。这时我们需要通过setlocale函数来设置如何将unicode编码的宽字符转换成一种可以输出的编码。

[Example2]
/* testwprintf.c , windows xp, mingw gcc-3.4.2 */
int main()
{
wchar_t ws[] = L”中文”;
setlocale(LC_ALL, “chs”); /* 设置gb码, unix上没有”chs”这样的locale,unix上可通过locale -a查 */
wprintf(L”%s\n”, ws);
}

setlocale(…)只在运行时起作用,这样编译执行后,”中文”二字就会显示在我们的控制台上了。

当然了我们还可以通过标准库调用将宽字符手动转成ANSI字符后再直接输出。

[Example3]
/* testwprintf.c , windows xp, mingw gcc-3.4.2 */
int main()
{
wchar_t ws[] = L”中文”;
char ms[12];
memset(&ms, 0, sizeof(ms));
setlocale(LC_ALL, “chs”); /* 设置gb码, unix上没有”chs”这样的locale,unix上可通过locale -a查 */
wcstombs(ms, ws, sizeof(ms));
printf(“%s\n”, ms);
}

编译执行后,”中文”二字同样跃然纸上。wcstombs是将宽字符串按照setlocale设置的编码转成指定的ANSI编码字符串;而mbstowcs 则是按照etlocale设置的编码将将多字节字符串转换成unicode编码存储在宽字符串中。前者调用setlocale是指导目标编码的;后者调用 setlocale的作用是指导如何将源字符串翻译成目的unicode字符串的。类似的还有字符级别的标准函数:wctomb和mbtowc。

关于字符编码转换,其实有很多好用的开源工具包可用,比如著名的iconv,自己平时很少会去实现一个编码转换。学习以上知识只是为了让自己再遇到乱码问题的时候不再迷糊,而且对计算机字符编码知识有一个概念上的了解是必要的且大有裨益的。

2011年10月30日 | 归档于 Computer Science

linux 简单svn命令 (待续)

由于在开发机上写代码,每次写完提交都要用到svn命令,所以将几个常用的命令先简单的记录一下。以后再慢慢更新。

注:[ ]表示里面不是必须参数

从服务器上checkout :  svn checkout path
添加新文件                 :  svn  add filename
提交新版本                 :  svn commit –m “日志信息” [文件名 or 路径]
查看版本状态             :   svn status [-v]   若不加-v选项,状态正常就不显示。只显示异常状态。加-v显示所有状态
查看版本变更日志       :   svn log [文件名 or 路径]
删除文件或文件夹       :   svn delete [文件名 or 路径] –m “日志信息
更新到版本m              :   svn update [-r m] [文件名 or 路径]   没有-r m则更新至最新版本

2011年9月8日 | 归档于 Computer Science, Trick
标签: ,

单CPU上使用多线程(原 + 转)

使用多线程的目的是提高CPU的利用率,使每个线程在在宏观上看起来是在同一时刻执行的(微观上,同一时刻只有一个线程在执行)。

也就是说,如果程序是CPU密集型的,在同一个CPU上运行多个线程并不能提高CPU利用率。如果程序涉及到大量的低速操作,如IO,那么可以利用多线程,在某个线程进行IO的时候,其他线程占用CPU进行计算。

另一方面,多线程可以提高界面响应速度。比如在打游戏的时候,造坦克的速度比造小兵要慢。如果使用单线程,那么只有等到坦克建造结束之后才能开始造小兵。使用多线程的话,看到的是坦克和小兵在同时建造(两个指针转动)。

======================================================

以下内容转载自http://www.speedvi.net/2010/01/27/201.html

假设我们略微修改我们的例子,让它能演示有些时候在单处理器上使用多线程的好处。

在这个修改的例子里面,网络中的一个节点负责计算扫描线(与前面的图像例子一样)。不过,当一个扫描线的计算结束后,它的数据就通过网络发送到另外一个节点。下面是我们修改后的main()函数:

int
main (int argc, char **argv)
{
    int x1;

        // perform initializations

    for (x1 = 0; x1 < num_x_lines; x1++) {
        do_one_line (x1);           // "C" in our diagram, below
        tx_one_line_wait_ack (x1);  // "X" and "W" in diagram below
    }
}

 

你可以看出,我们已经消除了显示部分的代码,并添加了一个tx_one_line_wait_ack()函数。并进一步假设我们用的是较慢的网络,并且CPU并不参与网络传输的工作,它只是把数据发送到硬件,之后这个硬件来完成数据的传输。tx_one_line_wait_ack()函数使用了一点CPU来把数据发送到硬件,之后在等待远端的接收信号的时候是不使用CPU的。

下面的是CPU的使用框图(C表示了图像计算部分,X表示传输部分,W表示等待接收确认部分):

dpt3

我们可以看到在等待硬件完成它们的工作的时候,浪费了大量宝贵的CPU时间。如果使用多线程的话,我们就可以更好的利用CPU了,如下图所示:

dpt4_thumb

在这幅图里面,我们可以看到情况就好些了。虽然在第二个线程里面仍然会花费一些时间等待,不过总体来说我们消减了总的计算时间。

如果我们的时间中,Tcompute 用于计算,Ttx 用于传输,Twait 用于硬件传输,在第一个情况下,我们总的运行时间是:

(Tcompute + Ttx + Twait) × num_x_lines

而在第二个情况下,总的运行时间是:

(Tcompute + Ttx) × num_x_lines + Twait

中间减少的时间为:

Twait × (num_x_lines – 1)

显然,TwaitTcompute。如果我们在有4个CPU的SMP系统上运行4线程的版本,那么运行情况就像下面这样:

dpt5

 

可以看到,每个CPU都没有被充分利用(在utilization图中的空格表示)。在上面的图中有两个有趣的地方。当这4个线程开始时,它们都开始计算。不幸的是当这些线程完成计算之后,它们就开始对传输硬件的竞争。(在图中的X部分有偏移,这是因为同一时刻只能有一个正在进行的传输)。这在一开始就有一些异常。一旦线程过了这个阶段,它们就自然的与传输硬件同步了,因为传输所花的时间只是计算周期的四分之一。忽略一开始的异常,这个系统就可以使用如下公式描述了:

(Tcompute + Ttx + Twait) × num_x_lines / num_cpus

 

这个公式说明了,在4个CPU上面使用4个线程要比我们一开始的单线程模式快4倍。

通过结合后面的单CPU上面使用多线程的方法,我们可以让线程数大于CPU的数目,这样多出的线程就能够使用因为传输等待而产生的空闲时间了。这样的运行情况就会像下面这样:

dpt5a

 

在这里,有如下几个假设:

  • 线程5、6、7、8分别绑定到1、2、3、4号处理器;
  • 传输部分的优先级比计算部分的优先级高;
  • 传输为不可中断的操作。

在上面的示意图中,可以发现即使我们有了两倍于CPU数量的线程,在运行的过程中有时还是有CPU未完全使用的情况。在图中有三个地方,CPU是低效使用的,这在每个CPU的使用带状图中使用数字进行了标注:

    1. 线程1在等待接收确认(W状态),同时线程5完成了计算并等待传输;
    2. 线程2与6都在等待确认;
    3. 线程3在等待确认的时候线程7完成了计算并等待传输。

这个例子同时也可以得出一个结论,就是你不能通过不停的添加CPU来让程序运行的更快些,因为还有其他的受限因素。有些时候,这个受限因素是多处理器的主板的设计——当多个CPU试图访问同一区域的内存时会产生多少的内存与设备的竞争。在我们的例子里面,我们可以看到TX Slot Utilization条形图开始变满了。如果我们添加足够多的CPU,在运行的时候必然会出问题,因为它们的线程要等待传输,而是低速运行的。

不论如何,都是可以使用大量的线程来利用空闲的CPU的,这样可以更好的利用CPU。利用的公式大概像下面这样:

(Tcompute + Ttx) × num_x_lines / num_cpus

这个计算的本质就是我们只是受限于我们使用的CPU的个数;我们不让任何CPU因为等待响应而空闲。(当然了,这是理想化的。在上面的框图中,有几次是周期性的让一个CPU空闲的,Tcompute + Ttx × num_x_lines是我们速度的限制)

重载(overload)、覆盖(override)、隐藏(hide)的区别 (转载)

这三个概念都是与OO中的多态有关系的。如果单是区别重载与覆盖这两个概念是比较容易的,但是隐藏这一概念却使问题变得有点复杂了,下面说说它们的区别吧。

重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。
覆盖(也叫重写)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。
隐藏是指派生类中的函数把基类中相同名字的函数屏蔽掉了。隐藏与另外两个概念表面上看来很像,很难区分,其实他们的关键区别就是在多态的实现上。什么叫多态?简单地说就是一个接口,多种实现吧。

还是引用一下别人的代码来说明问题吧(引用自林锐的《高质量C/C++编程指南》)。

01 #include <iostream>
02
03 using namespace std;
04
05 class Base 
06 { 
07 public: 
08     virtual void f(float x)
09     {
10         cout << "Base::f(float) " << x << endl;
11     } 
12     void g(float x)
13     {
14         cout << "Base::g(float) " << x << endl;
15     }
16     void h(float x)
17     {
18         cout << "Base::h(float) " << x << endl;
19     } 
20 }; 
21
22 class Derived : public Base
23 { 
24 public: 
25     virtual void f(float x)
26     {
27         cout << "Derived::f(float) " << x << endl;
28     } 
29     void g(int x)
30     {
31         cout << "Derived::g(int) " << x << endl;
32     }
33     void h(float x)
34     {
35         cout << "Derived::h(float) " << x << endl;
36     }
37 }; 
38
39 int main()
40 {
41     Derived  d
42     Base *pb = &d
43     Derived *pd = &d;
44     
45     // Good : behavior depends solely on type of the object 
46     pb->f(3.14f); // Derived::f(float) 3.14 
47     pd->f(3.14f); // Derived::f(float) 3.14 
48
49     // Bad : behavior depends on type of the pointer 
50     pb->g(3.14f); // Base::g(float) 3.14 
51     pd->g(3.14f); // Derived::g(int) 3        (surprise!) 
52
53     // Bad : behavior depends on type of the pointer 
54     pb->h(3.14f); // Base::h(float) 3.14      (surprise!) 
55     pd->h(3.14f); // Derived::h(float) 3.14 
56    
57     return 0;
58 }

 

看出什么了吗?下面说明一下:
(1) 函数Derived::f(float)覆盖了Base::f(float)。
(2)
函数Derived::g(int)隐藏了Base::g(float),而不是重载。
(3)
函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。

在第一种调用中,函数的行为取决于指针所指向的对象。在第二第三种调用中,函数的行为取决于指针的类型。所以说,隐藏破坏了面向对象编程中多态这一特性,会使得OOP人员产生混乱。

不过隐藏也并不是一无是处,它可以帮助编程人员在编译时期找出一些错误的调用。但我觉得还是应该尽量使用隐藏这一些特性,该加virtual时就加吧。

成员函数被重载的特征
(1)相同的范围(在同一个类中);

(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。

覆盖是指派生类函数覆盖基类函数,特征是
(1)不同的范围(分别位于派生类与基类);

(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
(5)返回值相同。先通过1-4判断是否符合覆盖的基本概念,如果符合,但是返回值不同,则是一个错误的覆盖,导致编译错误。

“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

3种情况怎么执行:
1.重载:看参数
2.隐藏:用什么就调用什么
3.覆盖:调用派生类

例1:

01 #include <iostream>
02
03 using namespace std;
04
05 class CB
06 {
07 public:
08     void f(int)
09     {
10         cout << "CB::f(int)" << endl;
11     }
12 };
13
14 class CD : public CB
15 {
16 public:
17     void f(int,int)
18     {
19         cout << "CD::f(int,int)" << endl;
20     }
21     void test()
22     {
23         f(1);
24     }
25 };
26 int main(int argc, char* argv[])
27 {
28     return 0;
29 }

结论:在类CD这个域中,没有f(int)这样的函数,基类中的void f(int)被隐藏
如果把派生CD中成员函数void f(int,int)的声明改成和基类中一样,即f(int),基类中的void f(int)还是一样被隐藏,此时编译不会出错,在函数中test调用的是CD中的f(int)。
所以,在基类中的某些函数,如果没有virtral关键字,函数名是f(参数是什么我们不管),那么如果在派生类CD中也声明了某个f成员函数,那么在类CD域中,基类中所有的那些f都被隐藏。
如果你比较心急,想知道什么是隐藏,看文章最后的简单说明,不过我建议你还是一步一步看下去。我们刚才说的是没有virtual的情况,如果有virtual的情况呢??

例2:

01 #include <iostream>
02
03 using namespace std;
04
05 class CB
06 {
07 public:
08     virtual void f(int)
09     {
10         cout << "CB::f(int)" << endl;
11     }
12 };
13
14 class CD : public CB
15 {
16 public:
17     void f(int)
18     {
19         cout << "CD::f(int,int)" << endl;
20     }
21     void test()
22     {
23         f(1);
24     }
25 };
26 int main(int argc, char* argv[])
27 {
28     return 0;
29 }

这么写当然是没问题了,在这里我不多费口舌了,这是很简单的,多态,虚函数,然后什么指向基类的指针指向派生类对象阿,通过引用调用虚函数阿什么的,属性多的很咯,什么??你不明白??随便找本C++的书,对会讲多态和虚函数机制的哦!!

这种情况我们叫覆盖(override)!覆盖指的是派生类的虚拟函数覆盖了基类的同名且参数相同的函数!
在这里,我要强调的是,这种覆盖,要满足两个条件
(a) 有virtual关键字,在基类中函数声明的时候加上就可以了
(b) 基类CB中的函数和派生类CD中的函数要一模一样,什么叫一模一样,函数名,参数,返回类型三个条件。

有人可能会对(b)中的说法质疑,说返回类型也要一样??
是,覆盖的话必须一样,我试了试,如果在基类中,把f的声明改成virtual int f(int),编译出错了
error C2555: ‘CD::f’ : overriding virtual function differs from ‘CB::f’ only by return type or calling convention。所以,覆盖的话,必须要满足上述的(a)(b)条件。

那么如果基类CB中的函数f有关键字virtual,但是参数和派生类CD中的函数f参数不一样呢?
例3:

01 #include <iostream>
02
03 using namespace std;
04
05 class CB
06 {
07 public:
08     virtual void f(int)
09     {
10         cout << "CB::f(int)" << endl;
11     }
12 };
13
14 class CD : public CB
15 {
16 public:
17     void f(int, int)
18     {
19         cout << "CD::f(int,int)" << endl;
20     }
21     void test()
22     {
23         f(1);
24     }
25 };
26 int main(int argc, char* argv[])
27 {
28     return 0;
29 }

编译出错了,error C2660: ‘f’ : function does not take 1 parameters
咦??好面熟的错??对,和实例一中的情况一样哦,结论也是基类中的函数被隐藏了。
通过上面三个例子,得出一个简单的结论:
如果基类中的函数和派生类中的两个名字一样的函数f满足下面的两个条件:
(a)在基类中函数声明的时候有virtual关键字
(b)基类CB中的函数和派生类CD中的函数一模一样,函数名,参数,返回类型都一样。
那么这就是叫做覆盖(override)这也就是虚函数,多态的性质
那么其他的情况呢??只要名字一样,不满足上面覆盖的条件,就是隐藏了。
下面我要讲最关键的地方了,好多人认为,基类CB中的f(int)会继承下来和CD中的f(int,int)在派生类CD中构成重载,就像实例一中想像的那样。对吗?我们先看重载的定义
重载(overload):
必须在一个域中,函数名称相同但是函数参数不同,重载的作用就是同一个函数有不同的行为,因此不是在一个域中的函数是无法构成重载的,这个是重载的重要特征
必须在一个域中,而继承明显是在两个类中了哦,所以上面的想法是不成立的,我们测试的结构也是这样,派生类中的f(int,int)把基类中的f(int)隐藏了所以,相同的函数名的函数,在基类和派生类中的关系只能是覆盖或者隐藏。
在文章中,我把重载和覆盖的定义都给了出来了,但是一直没有给隐藏的定义,在最后,我把他给出来,这段话是网上google来的,比较长,你可以简单的理解成,在派生类域中,看不到基类中的那个同名函数了,或者说,是并没有继承下来给你用,呵呵,如实例一那样。
隐藏(hide):
指的是派生类的成员函数隐藏了基类函数的成员函数.隐藏一词可以这么理解:在调用一个类的成员函数的时候,编译器会沿着类的继承链逐级的向上查找函数的定义,如果找到了那么就停止查找了,所以如果一个派生类和一个基类都有同一个同名(暂且不论参数是否相同)的函数,而编译器最终选择了在派生类中的函数,那么我们就说这个派生类的成员函数"隐藏"了基类的成员函数,也就是说它阻止了编译器继续向上查找函数的定义。

2011年7月23日 | 归档于 C/C++, Computer Science
标签: ,

用shell统计源代码行数

这两天写了个小程序,突然想统计一下源代码行数,而且觉得以后也需要用得到。于是在网上找了找,发现用只需要一行shell代码就可以搞定了。(对于我的工程目录来说)

我的工程目录大致结构为:
.
..
include/
            /json/
            /a.h
lib/
obj/
src/
     /a.cpp
Makefile

源代码全部放在src/文件夹之下,自定义的头文件放在include/文件夹之下,用到的其他头文件作为单独的目录放在include/之下。

find . -maxdepth 2  \( -name "*.cpp" -o -name "*.h"  \) -type f -exec cat {} \; | wc –l

在工程根目录下执行,指定maxdepth为2,不然会统计到include/json/之下的代码行数。两个-name条件需要用括号括起来(否则执行的结果不正确,只有include下的代码行数,这一点我也不知道为什么)。指定文件类型,以及执行的函数,最后进行统计就可以了。

当然,可以根据自己的工程目录结构修改。

2011年6月21日 | 归档于 Computer Science, Linux
标签: , ,

C++中的行指针和列指针

是我之前BLOG上面的一篇,今天写代码的时候发现对这些基础的概念有些模糊了。因此将文件搬过来,以后查起来就更方便了。全是代码,注释已经写在代码里面了。

#include <iostream> 
using namespace std
int main() 
{ 
    int s1[3]; 
    int (*p1)[3] = s1;    // error, s1为列指针,p1为数组指针,这里理解为行指针 
    int (*p2)[3] = &s1;   // ok, 列指针取地址即为行指针 
    int  *p3     = s1;    // ok 
         
    int s2[3][4]; 
    int (*p4)[3][4] = s2; // error, s2理解为为行指针,即int (*)[4],指向一个一维数组, p4指向的是一个二维数组 
    int (*p5)[3][4] = &s2;// ok, 取数组的地址赋给数组的指针 
    int (*p6)[4] = s2;    // ok, 见14行 
    int (*p7)[4] = s2[0]; // error, s2[0]有两个含义,1,一个一维数组的数组名,2.一个列指针,即int*, 指向数组的第一行第一列 
    int  *p8 = s2[0];     // ok, p9指向第一行的第一个元素 
    int  *p9 = s2[0] + 1; // ok, p9指向第一行的第二个元素 
         
    cout << sizeof(s1) << endl;          // 12, 数组名 
    cout << sizeof(s1[0]) << endl;        // 4, int 
    cout << sizeof(s2[0]) << endl;       // 16, 数组名 (也有列指针的含义) 
    cout << sizeof(s2[0] + 1) << endl;   // 4, 列指针 
    cout << sizeof(s2) << endl;          // 48, 数组名 
    return 0
}
2011年6月15日 | 归档于 C/C++, Computer Science
标签: ,

[转载] GCC编译选项

转帖过来,忘记的时候可以参考。

Gcc、g++分别是gnu的c & c++编译器 gcc/g++在执行编译工作的时候,共需要4步

1.预处理,生成.i的文件[预处理器cpp]
2.将预处理后的文件不转换成汇编语言,生成文件.s[编译器egcs]
3.有汇编变为目标代码(机器代码)生成.o的文件[汇编器as]
4.连接目标代码,生成可执行程序[链接器ld]

[参数详解]
-x language filename
设定文件所使用的语言,使后缀名无效,对以后的多个有效.也就是根据约定C语言的后缀名称是.c的,而C++的后缀名是.C或者.cpp,如果你很个性,决定你的C代码文件的后缀名是.pig 哈哈,那你就要用这个参数,这个参数对他后面的文件名都起作用,除非到了下一个参数的使用。
可以使用的参数吗有下面的这些
`c’, `objective-c’, `c-header’, `c++’, `cpp-output’, `assembler’, and `assembler-with-cpp’.
看到英文,应该可以理解的。
例子用法:
gcc -x c hello.pig

-x none filename
关掉上一个选项,也就是让gcc根据文件名后缀,自动识别文件类型
例子用法:
gcc -x c hello.pig -x none hello2.c

-c
只激活预处理,编译,和汇编,也就是他只把程序做成obj文件
例子用法:
gcc -c hello.c
他将生成.o的obj文件

-S
只激活预处理和编译,就是指把文件编译成为汇编代码。
例子用法
gcc -S hello.c
他将生成.s的汇编代码,你可以用文本编辑器察看

-E
只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面.
例子用法:
gcc -E hello.c > pianoapan.txt
gcc -E hello.c | more
慢慢看吧,一个hello word 也要与处理成800行的代码

-o
制定目标名称,缺省的时候,gcc 编译出来的文件是a.out,很难听,如果你和我有同感,改掉它,哈哈
例子用法
gcc -o hello.exe hello.c (哦,windows用习惯了)
gcc -o hello.asm -S hello.c

-pipe
使用管道代替编译中临时文件,在使用非gnu汇编工具的时候,可能有些问题
gcc -pipe -o hello.exe hello.c

-ansi
关闭gnu c中与ansi c不兼容的特性,激活ansi c的专有特性(包括禁止一些asm inline typeof关键字,以及UNIX,vax等预处理宏,

-fno-asm
此选项实现ansi选项的功能的一部分,它禁止将asm,inline和typeof用作关键字。

-fno-strict-prototype
只对g++起作用,使用这个选项,g++将对不带参数的函数,都认为是没有显式的对参数 的个数和类型说明,而不是没有参数. 而gcc无论是否使用这个参数,都将对没有带参数的函数,认为城没有显式说明的类型。

-fthis-is-varialble
就是向传统c++看齐,可以使用this当一般变量使用.

-fcond-mismatch
允许条件表达式的第二和第三参数类型不匹配,表达式的值将为void类型

-funsigned-char
-fno-signed-char
-fsigned-char
-fno-unsigned-char

这四个参数是对char类型进行设置,决定将char类型设置成unsigned char(前两个参 数)或者 signed char(后两个参数)

-include file
包含某个代码,简单来说,就是便以某个文件,需要另一个文件的时候,就可以用它设 定,功能就相当于在代码中使用#include<filename>
例子用法:
gcc hello.c -include /root/pianopan.h

-imacros file
将file文件的宏,扩展到gcc/g++的输入文件,宏定义本身并不出现在输入文件中

-Dmacro
相当于C语言中的#define macro

-Dmacro=defn
相当于C语言中的#define macro=defn

-Umacro
相当于C语言中的#undef macro

-undef
取消对任何非标准宏的定义

-Idir
在你是用#include “file”的时候,gcc/g++会先在当前目录查找你所制定的头文件,如果没有找到,,他回到缺省的头文件目录找,如果使用-I制定了目录,他会先在你所制定的目录查找,然后再按常规的顺序去找。对于#include <file>,gcc/g++会到-I制定的目录查找,查找不到,然后将到系统的缺省的头文件目录查找。

-I-
就是取消前一个参数的功能,所以一般在-Idir之后使用

-idirafter dir
在-I的目录里面查找失败,讲到这个目录里面查找.

-iprefix prefix
-iwithprefix dir
一般一起使用,当-I的目录查找失败,会到prefix+dir下查找

-nostdinc
使编译器不再系统缺省的头文件目录里面找头文件,一般和-I联合使用,明确限定头文件的位置

-nostdin C++
规定不在g++指定的标准路经中搜索,但仍在其他路径中搜索,.此选项在创libg++库使用

-C
在预处理的时候,不删除注释信息,一般和-E使用,有时候分析程序,用这个很方便的

-M
生成文件关联的信息。包含目标文件所依赖的所有源代码你可以用gcc -M hello.c 来测试一下,很简单。

-MM
和上面的那个一样,但是它将忽略由#include<file>造成的依赖关系。

-MD
和-M相同,但是输出将导入到.d的文件里面

-MMD
和-MM相同,但是输出将导入到.d的文件里面

-Wa,option
此选项传递option给汇编程序;如果option中间有逗号,就将option分成多个选项,然后传递给会汇编程序

-Wl.option
此选项传递option给连接程序;如果option中间有逗号,就将option分成多个选项,然后传递给会连接程序.

-llibrary
制定编译的时候使用的库
例子用法
gcc -lcurses hello.c
使用ncurses库编译程序

-Ldir
制定编译的时候,搜索库的路径。比如你自己的库,可以用它制定目录,不然编译器将只在标准库的目录找。这个dir就是目录的名称。

-O0 -O1 -O2 -O3
编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高

-g
只是编译器,在编译的时候,产生调试信息。

-gstabs
此选项以stabs格式声称调试信息,但是不包括gdb调试信息.

-gstabs+
此选项以stabs格式声称调试信息,并且包含仅供gdb使用的额外调试信息.

-ggdb
此选项将尽可能的生成gdb的可以使用的调试信息.

-static
此选项将禁止使用动态库,所以,编译出来的东西,一般都很大,也不需要什么动态连接库,就可以运行.

-share
此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.

-traditional
试图让编译器支持传统的C语言特性

2011年6月10日 | 归档于 Computer Science, Linux
标签: ,

简单学习了一下Linux system V 共享内存

这两天写一个小程序,第一次简单的使用了一下共享内存。在这里记录一下。

共享内存是进程间通信的方式之一。(进程是一个执行中的程序。每个进程有自己的地址空间,包括代码段,数据段和栈。代码段存放执行的代码,数据段存放全局数据以及动态分配的数据,栈区存放局部变量)。由于每个进程的地址空间是独立的,因此,进程间的通信需要借助到其他方法。共享内存是其中的一种。

说的直白一点,相当于不同进程都拥有同一块内存区域的地址。

其原理是将一块共享内存区域对应shm文件系统中的一个文件。在内核中有一个叫做shmid_kernel的结构体,它将文件系统和共享内存区联系起来。并且每一个共享内存区都有一个shmdi_kernel结构作为控制结构。

struct shmid_kernel /* private to the kernel */
{   
    struct kern_ipc_perm    shm_perm;
    struct file *        shm_file;
    int            id;
    unsigned long        shm_nattch;
    unsigned long        shm_segsz;
    time_t            shm_atim;
    time_t            shm_dtim;
    time_t            shm_ctim;
    pid_t            shm_cprid;
    pid_t            shm_lprid;
};

其中shm_file域存储了shm文件系统中的特殊文件的地址,该文件不属于任何一个进程。

对于系统中的IPC资源,系统之中会用一个全局变量来描述该类资源的公有数据。描述共享内存的全局变量叫做shmid_ds(信号量对应的变量叫做semid_ds,消息对应的叫做msgid_ds),它是一个ipc_ids的结构体,其定义如下:

struct ipc_ids
{
    int size;
    int in_use;
    int max_id;
    unsigned short seq;
    unsigned short seq_max;
    struct semaphore sem;  
    spinlock_t ary;
    struct ipc_id* entries;
};

其中最后一项entries是一个指针,指向所有该类资源的一个数组。每一个具体的资源对应一个ipc_id结构体,其定义如下:

struct ipc_id
{
    struct kern_ipc_perm* p;
};

这里的kem_ipc_perm刚好和上面第一类结构体中的kem_ipc_perm相对应起来了。因此,shmid_ds这个全局变量记录了每一块共享内存区的信息。每一块共享内存区都由一个shmid_kernel控制,和一个特殊的shm文件系统中的文件相对应。

上面提到的共享内存方式叫做系统V方式。它有几个固定的API,像我这种简单使用者来还是比较容易的。一般的步骤是:
1.先通过ftok函数获取一个系统V IPC key。
2.用shmget函数获得共享内存区的ID。
3.用shmat函数将第二步获得的ID映射到每个进程自己的地址空间。
4.进程使用完共享内存之后,用shmdt接触进程对该区域的映射。
5.最后共享内存区做控制操作,比如删除。(共享内存是随内核持续存在的,如果不手动删除,即使进程结束仍然存在。)

要注意的是,访问共享内存的时候,要采取同步机制。

目前,对于linux下的共享内存,我只了解到这个浅显的地步。其他的有关内存页面的东西,只能以后慢慢学习了。

2011年5月27日 | 归档于 Computer Science, Linux
标签: ,