随笔 - 21  文章 - 0 评论 - 10 trackbacks - 0

搜索

 

随笔分类

最新随笔

     摘要: 目录(*) 数组的排序复杂度O(n*n)的有selection sortbubble sortrank sort下面对这三种算法进行讲解:selection sort思路:首先找出最大的元素,把它移动到最后(即a[n-1]的位置上),然后在余下的n-1个元素中找出最大的,移动到a[n-2],如此进行下去直到只剩下一个元素。[代码]代码中用临时变量max来保存最大元素,而不用a[idxOfMax],... 阅读全文
posted @ 2008-07-04 00:56 董超 阅读(46) | 评论 (0)编辑

什么样的问题可以用递归?
其实很多问题都可以用递归解决,例如数列的求和:

#include <iostream>
using namespace std;

template 
<class T>
T    recrusive_sum(T a[], 
int idx)
{
    
if(idx == 0// 退出条件
        return a[0];
    
else
        
return a[idx] + recrusive_sum(a, idx - 1); // 使用内层返回的结果
}

int main(void)
{
    
const int elem_cnt = 100;
    
int a[elem_cnt];
    
for(int i = 0; i < elem_cnt; ++i)
        a[i] 
= i + 1;
    cout 
<< recrusive_sum(a, elem_cnt - 1<< endl;
    
return 0;
}

显然,这不是一个高效的算法,我们通常用率更高的迭代法来解决上面的问题。用这个例子只是想说明,很多问题可以用递归解决。
能用递归解决的问题通常具有两个特点:
1  有退出条件
2  外层需要用到内层算出的结果(也可能是内层需要外层的计算结果,但比较少见)
最难的地方是找出外层利用内层结果的方法,这往往需要在思考问题的过程中发现规律,纸笔是不可缺少的。
另外退出条件需要拿捏准确,这也是一个容易出错的地方。

下面是求全排列和求全部子集的算法,注意以上两点在代码中的体现。

(*) 求全排列
不妨写出一个简单例子,我们用P(a,b,c)表示a,b,c的全排列,
则P(a,b,c)=
a b c
a c b
b a c
b c a
c a b
c b a
我们发现,以上结果按首字母可划分为三组,它们是
a + P(b,c)
b + P(a,c)
c + P(a,b)
其实就是第一个字母轮换,其余两个位置是剩下两个字母的全排列。“剩下两个字母的全排列”正是我们可以利用的内层结果。
代码如下:

#include <iostream>
using namespace std;

template 
<class T>
void    perm(T list[], int begin, int end)
{
    
if(begin == end){
        
for(int i = 0; i <= end; ++i)
            cout 
<< list[i] << '\t';
        cout 
<< endl;
    }
    
else{
        
for(int i = begin; i <= end; ++i){
            swap(list[begin], list[i]);            
            perm(list, begin 
+ 1, end);            
            swap(list[begin], list[i]);
        }
    }
}

int main()
{
    
char a[] = {'1''2''3''4'};
    
int min_idx = 0;
    
int max_idx = sizeof a / sizeof *- 1;
    perm(a, min_idx, max_idx);
    
return 0;
}


(*) 求所有子集

不妨写一个简单的例子,然后从中发现规律。例如,集合{a,b,c}的子集有:
{}
{a}
{b}
{a, b}
{c}
{a, c}
{b, c}
{a, b, c}
耐心分析结果,发现:
{}                                (0)
{a} = {a} + {}                    (a)
{b} = {b} + {}                    (b)
{a, b} = {b} + {} + {a}           (b)
{c} = {c} + {}                    (c)
{a, c} = {c} + {a}                (c)
{b, c} = {c} + {b}                (c)
{a, b, c} = {c} + {a, b}          (c)
其实我们能从上面的分析过程中得到不只一条结论:
1 所有的子集总数是二项展开式系数和C(n,0)+C(n,1)+...+C(n,n)=2^n.这个结论虽然对解决本题没什么帮助,但它应该,也是最容易被注意到的。
2 我们将上面的所有子集分组,发现从最简单的空集开始,新出现的组都是新元素和之前所有组的笛卡尔积。这正是递归利用内层计算结果的地方。代码如下:

#include <iostream>
#include 
<vector>
#include 
<string>
using namespace std;

template 
<class T>
void    subset(vector< vector<T> >& res, const vector<T>& src, int idx);

template 
<class T>
void    show_1d(const vector<T>& vec);

template 
<class T>
void    show_2d(const vector< vector<T> >& vec);

template 
<class T>
void    append_cartesian(vector< vector<T> >& res, T appendant);

int main()
{
    vector
<string>        src;
    vector
< vector<string> >    res;
    
    src.push_back(
"c");
    src.push_back(
"b");
    src.push_back(
"a");

    subset(res, src, 
0);

    show_2d(res);
    
return 0;
}

template 
<class T>
void    subset(vector< vector<T> >& res, const vector<T>& src, int idx)
{
    
if(src.size() == idx){        
        vector
<T> empty_set;
        res.push_back(empty_set);
// append an empty vector when reach base
    }
    
else{
        subset(res, src, idx 
+ 1);
        append_cartesian(res, src[idx]);
//将内层算出的结果笛卡尔积到本层
    }
}

template 
<class T>
void    append_cartesian(vector< vector<T> >& res, T appendant)
{
    
int len = res.size();
    
for(int i = 0; i < len; ++i){
        vector
<T> tmp = res[i];
        tmp.push_back(appendant);
        res.push_back(tmp);
    }
}

template 
<class T>
void    show_1d(const vector<T>& vec)
{
    cout 
<< '{';
    
for(int i = 0; i < vec.size(); ++i){
        cout 
<< vec[i];
        
if(i != vec.size() - 1)
            cout 
<< "";
    }
    cout 
<< '}' << endl;
}

template 
<class T>
void    show_2d(const vector< vector<T> >& vec)
{
    
for(int i = 0; i < vec.size(); ++i)
        show_1d(vec[i]);
}


 

posted @ 2008-06-29 18:57 董超 阅读(51) | 评论 (0)编辑
什么是信息?在计算机世界,信息就是位的序列。这么说它很重要咯?那我们就来讨论一些实用的位运算技巧吧。

(*) 危险的unsigned
在C/C++里有unsigned char/short/int/long。它和默认的signed的不同在于头一位不当作符号,而当作数值的一部分。
尽量避免unsigned在加减乘除和比较运算中出现,否则可能产生意想不到的错误。例如:
unsigned u = -1;
int i = 1;
if(i > u) // 按说1>-1是不争的事实,但结果并非如此
原因是signed int和unsigned int做比较的时候,会把signed int提升成unsigned int.所以实际上是两个无符号整数0x00000001和0xffffffff在比较。
通常只在位运算中才使用unsigned,原因是unsigned类型不会进行符号扩展。下面一段C++代码很好的展示了这一点。
char ch = 128;
unsigned 
char uch = 128;
short ui;
// 整数提升时的符号位扩展            
ui = ch;
cout 
<< hex << ui << endl; // 输出: ff80
ui = uch;
cout 
<< ui << endl; // 输出: 80
cout << short(ch) << endl; // 输出: ff80
cout << short(uch) << endl; // 输出: 80
// 右移时的符号位扩展
cout << (ch >> 1<< endl; // 输出: ffffffc0
cout << (uch >> 1<< endl; // 输出: 40
// 注意,在进行位运算后,会把字符提升为整数,可用下面代码进行验证:    
cout << typeid(ch >> 1).name() << endl; // 输出: int
cout << typeid(ch | 0).name() << endl; // 输出: int


(*) 得到形如00001111 和 11110000的掩码
 // 形如11110(n)
mask = -1 << n;


// 形如00001(n)
mask = ~(-1 << n); 或者 mask = (1 << n) - 1;

// 形如0(n)1111
mask = -1u >> n;
或者
mask = -1 << ((sizeof mask * 8) - n);  (sizeof i * 8 是先算出mask所占的字节数,再乘8得到总位数)

// 形如1(n)0000
mask = ~(-1u >> n);
或者
mask = -1 << ((sizeof mask * 8) - n);
虽然在代码写法上第一种方法比第二种方法简单,但它可能报警告unary minus operator applied to unsigned type, result still unsigned。而且第二种方法由于在编译时就已经计算出了(sizeof mask * 8)的值,所以运行效率不比第一种差。


(*) 提高mod计数器效率
让seq在0...N之间循环递增,一般会想到用mod运算:
seq = (seq + 1) % (N + 1);
但mod运算消耗较高,不如以下的方法:
if(++seq > N)
 seq = 0;

如果N是2的幂减1(二进制如00011111),可以用下面更快的方法
seq = ++seq & N;
再强调一遍,这种算法只有在N是2的幂减1时才能用。而事实上,为了充分利用整数中的每一个位,往往都把序号的上限设计成这种形式,如0xefffffff。


(*) 不用临时变量的swap

这是一道常考的面试题。有两种解法最为常见:

= x + y;
= x - y; // x + y - y == x + (y - y) == x + 0 == x
= x - y; // x + y - x == y + (x - x) == y + 0 == y

= x ^ y;
= x ^ y; // x ^ y ^ y == x ^ (y ^ y) == x ^ 0 == x
= x ^ y; // x ^ y ^ x == y ^ (x ^ x) == y ^ 0 == y

第一种不难理解,而第二种用异或操作就比较令人费解了。原因是大部分人不知道异或的逆运算正是异或本身。只要掌握了“逆运算”的要领,这道题还可以有其他很多种解法,例如:

= x * y;
= x / y; // x * y / y == x
= x / y; // x * y / x == y

在所有方法中异或法是最好的,因为用加减或乘除可能导致溢出。
但事实上,以上所有这些swap的方法都是不保险的。例如当x和y是同一个变量的引用:

int a = 1;    
int& x = a;
int& y = a;    
= x ^ y; // x = 1 ^ 1 == 0
= x ^ y; // y = 0 ^ 0 == 0
= x ^ y; // x = 0 ^ 0 == 0

因此,在实际编码中不要耍花招,还是用临时变量吧。



(*) 位的置1与清0
学会了使用mask,位运算就算入门了。
置1: |=
清0: &=
判断某一位是否为1: &
例如:

#define BIT_SET(integer, offset)        ((integer) |= 1 << (offset))
#define BIT_CLR(integer, offset)        ((integer) &= ~(1 << (offset)))
#define BlT_ISSET(integer, offset)      ((integer) & (1 << (offset)))

但只有整型以及可看作整型的变量才能进行位运算,如果要把一块内存中的某一位置1或清0,就要费些脑筋了。
思路是:先定位到一个字节,再对该字节进行位运算。
例如,buf是一个char型指针,我们要把以buf为首地址,偏移量为offset的位置1,则:
buf[offset / 8] |= 1 << (offset % 8);
将这行代码进行效率优化,得到:
buf[offset >> 3] |= 1 << (offset & 7);
于是,最强悍的位置1和位清0函数诞生了:

void    bit_set(void *buf, int offset)
{
    ((
char*)buf)[offset >> 3|= 1 << (offset & 7);
}

void    bit_clr(void *buf, int offset)
{
    ((
char*)buf)[offset >> 3&= ~(1 << (offset & 7));
}

int    bit_isset(void *buf, int offset)
{
    
return ((char*)buf)[offset >> 3& 1 << (offset & 7);
}

要进一步提高效率,还可以把上面三个函数改写成inline或宏,只是在用宏的时候稍加注意就行。


(*) 内存对齐
在自制的内存管理器中,经常需要内存对齐,否则会有总线错误。举例来说,一个int型变量占4字节,则它在内存中的地址必须是4的倍数。
那么如何把一块内存对齐呢?换句话说,如何把一个整数(变量地址)圆整为某个数(其类型所占的字节数)的倍数呢?下面是使用了位运算的一种方法:

#include <iostream>
using namespace std;

#define _ALIGN(addr, T)        (   (unsigned)( (char*)addr + sizeof(T) - 1 ) & ~(sizeof(T) - 1)   )
int main()
{
    
char c;
    cout 
<< hex << (unsigned)&<< ' ' << _ALIGN(&c, double<< endl;    
}

解释一下宏_ALIGN(addr, T)
其中addr是要进行对齐的地址,T是按什么类型对齐的那个类型。
用(unsigned)把一个char*强制转换成整数,因为编译器不允许直接对一个char *进行位运算。
用(char*)强制转换成pointer to char,这样才能与后面加上的指针偏移量sizeof(T) - 1相吻合。
~00000111(b) == 11111000(b),这是一个掩码,为的是把它变成某个数(必须是2的N次幂)的倍数。而这个按位与其实是做减法,但我们只能向后对齐,所以要保证对齐后的内存不能比原来小,这就是+ sizeof(T) - 1的原因。


(*) 不要滥用移位来代替乘除
很多人学了位运算以后,就总想耍一些小把戏,比如乘除2的次幂都用移位来做。
但大部分人没有注意到,对于有符号整数,右移运算和除法不能等价。请看下例:
cout << "-1 >> 1 = " << (-1 >> 1)  << endl;
cout 
<< "-1 / 2 = " << (-1 / 2)  << endl;
cout 
<< "-3 >> 1 = " << (-3 >> 1)  << endl;
cout 
<< "-3 / 2 = " << (-3 /2)  << endl;
cout 
<< "-2 >> 1 = " << (-2 >> 1)  << endl;
cout 
<< "-2 / 2 = " << (-2 / 2)  << endl;
cout 
<< "-16 >> 1 = " << (-16 >> 1)  << endl;
cout 
<< "-16 / 2 = " << (-16 / 2)  << endl;
程序的输出结果:
-1 >> 1 = -1
-1 / 2 = 0
-3 >> 1 = -2
-3 / 2 = -1
-2 >> 1 = -1
-2 / 2 = -1
-16 >> 1 = -8
-16 / 2 = -8
我们发现,负奇数的运算有偏差,负偶数则没有问题。这是因为对整数的除法,当不能整除时采取“向原点取整”(在x86用VS2008编译),负奇数因为无法被2整除所以出现了偏差。如果采取向数轴负方向取整的策略,就不存在任何偏差了,但怎么取整不是我们能说了算的:)
向原点取整自有它的道理,只有向原点取整才能让下面等式成立:
-(1/2) == -1/2
为了我们的计算机不至于变成数学白痴,还是采用向原点取整吧:)
所以对有符号数,要尽量避免位运算。对无符号的类型,则可以放心大胆地使用。
posted @ 2008-06-09 21:45 董超 阅读(92) | 评论 (0)编辑
STL

Content
简介
容器
迭代器
算法
简介
首先为什么不叫标准库,而叫标准模板库呢?原因在于它突出模板(即泛型)的概念。

stl由三部分组成:容器,迭代器,算法。下面分别做一个简略介绍:
stl的哲学是将数据和操作分离。数据由容器管理,操作由可定制的算法定义,迭代器充当了两者之间的的粘合剂。
(*) 容器
序列式容器(sequence container): vector, deque, list
元素的位置取决于插入的时机
关联式容器(associative container): set, multiset(允许元素重复的set), map, multimap(允许元素重复的map)
元素的位置取决于排序规则。内部实现方式都是二叉树。

map其实可以看成set,只不过元素是pair,分成了key(first)和value(second),另外只有key不允许冲突。multimap则连key也可以重复。
用map可以用以下方式插入、修改元素,可当作关联数组来使用,这一点multimap做不到:
map<string, int> scores;
scores["James"] = 76;
scores["Stevens"] = 89;
multiple允许key重复,这一点map做不到。

(*) 迭代器
迭代器用来在一个容器内行进,而不管这个容器具体是数组、链表或是树。因为每个容器都提供了自己的迭代器的实现。
begin()指向第一个元素,end()指向最后一个元素的后面。若container为空,则begin() == end()。

迭代器只不过是“容器中某一位置”的抽象概念,它对自己所属的容器一无所知,任何以迭代器访问容器的算法都无法通过迭代器调用容器类型的成员函数。

尽量用iter != end()来判断结尾,而不是iter < end(),因为只有vector, deque, list的迭代器才支持<运算符。

(*) 算法
算法并非容器的成员函数,而是搭配迭代器使用的全局函数。

算法vs成员函数
总的来说,算法更通用,成员函数更有针对性。
成员函数的优点:
如果追求高效率,应优先选用成员函数。例如对list使用remove算法就不如使用.remove成员函数效率高。
成员函数的缺点:
一旦换另一种容器,就不得不更动程序代码.


容器

对应用程序员来讲,容器毫无疑问是STL中最有价值的部分。学习容器应从两个方面入手:
1 各种容器的适用情况
2 了解容器的接口
下面列出了各种容器的比较:



搜索元素并不一定要用set或map之类的二叉树结构,应具体情况具体分析。
例如,若元素个数较少,直接用vector就好,因为vector的结构是所有容器中最简单的。
如果对搜索性能要求极为苛刻,不妨用hash table,虽然它不在STL中,但很多库已经实现了hash table。

关于容器的使用细节,有一些资源可以参考:
网站:http://www.cplusplus.com/reference/stl/
书籍:C++ Standard Library  6.10


迭代器
Iterator Category Ability Providers
Input iterator Reads forward istream
Output iterator Writes forward ostream, inserter
Forward iterator Reads and writes forward  
Bidirectional iterator Reads and writes forward and backward list, set, multiset, map, multimap
Random access iterator Reads and writes with random access vector, deque, string, array

forward迭代器是input和output迭代器的结合,而且还能在同一个元素上“驻留”。
bidirectional迭代器在forward迭代器的基础上增加了回头遍历的功能,即--操作。
random access迭代器能使用数字索引,还能使用比较运算符比较同一容器中不同迭代器的前后位置。




算法
STL提供了几乎所有的常用算法。但这一部分内容相对琐碎,而且最好能预先掌握仿函数、iterator adapter这样的高级概念,门槛较高。所以先不深入了,等以后真用到再说。
仿函数参见C++ standard library Ch9
iterator adapter参见C++ standard library Ch7.4

posted @ 2008-05-21 09:57 董超 阅读(65) | 评论 (0)编辑
今天起,开始我的C++务实之旅。务实,就是少浪费时间,C++这门难缠的语言不值得浪费我们太多的生命。

务实,就要观其大略,莫纠缠于细节。
务实,就是用20%的时间学到80%有用的知识。

本系列以专题划分,各个专题并非一蹴而就,而是随学习的深入不断增添新内容,以备用时参考。
本系列侧重于“能做什么”而非“不能做什么”,侧重于“应该怎样做”而非“不该怎样做”。

如果您想通过本系列学习,最好具备以下条件:
1 学过C++
2 即将或正在使用C++编程

OK. Go!


下面是从一个好心肠的高手那里抄来的C++学习方法,值得时常回过头来看

C++的复杂性有两种分类办法,一是分为非本质复杂性和本质复杂性;其中非本质复杂性分为缺陷和陷阱两类。另一种分类办法是按照场景分类:库开发场景下的复杂性和日常编码的复杂性。从从事日常编码的实践者的角度来说,采用后一种分类可以让我们迅速掌握80%场景下的复杂性。

二八法则

以下通过列举一些常见的例子来解释这种分类标准:

80%场景下的复杂性:

1. 资源管理(C++日常复杂性的最主要来源):深拷贝&浅拷贝;类的四个特殊成员函数;使用STL;RAII惯用法;智能指针等等。

2. 对象生命期:局部&全局对象生存期;临时对象销毁;对象构造&析构顺序等等。

3. 多态

4. 重载决议

5. 异常(除非你不用异常):栈开解(stack-unwinding)的过程;什么时候抛出异常;在什么抽象层面上抛出异常等等。

6. undefined&unspecified&implementation defined三种行为的区别:i++ + ++i是undefined behavior(未定义行为——即“有问题的,坏的行为,理论上什么事情都可能发生”);参数的求值顺序是unspecified(未指定的——即“你不能依赖某个特定顺序,但其行为是良好定义的”);当一个double转换至一个float时,如果double变量的值不能精确表达在一个float中,那么选取下一个接近的离散值还是上一个接近的离散值是implementation defined(实现定义的——即“你可以在实现商的编译器文档中找到说明”)。这些问题会影响到你编写可移植的代码。

(注:以上只是一个不完全列表,用于演示该分类标准的意义——实际上,如果我们只考虑“80%场景下的复杂性”,记忆和学习的负担便会大大减小。)

20%场景下的复杂性:

1. 对象内存布局

2. 模板:偏特化;非类型模板参数;模板参数推导规则;实例化;二段式名字查找;元编程等等。

3. 名字查找&绑定规则

4. 各种缺陷以及缺陷衍生的workarounds(C++书中把这些叫做“技术”):不支持concepts(boost.concept_check库);类型透明的typedef(true-typedef惯用法);弱类型的枚举(强枚举惯用法);隐式bool转换(safe-bool惯用法);自定义类型不支持初始化列表(boost.assign库);孱弱的元编程支持(type-traits惯用法;tag-dispatch惯用法;boost.enable_if库;boost.static_assert库);右值缺陷(loki.mojo库);不支持可变数目的模板参数列表(type-list惯用法);不支持native的alignment指定。

(注:以上只是一个不完全列表。你会发现,这些细节或技术在日常编程中极少用到,尤其是各种语言缺陷衍生出来的workarounds,构成了一个巨大的长尾,在无论是C++的书还是文献中都占有了很大的比重,作者们称它们为技术,然而实际上这些“技术”绝大多数只在库开发当中需要用到。)

非本质复杂性&本质复杂性

此外,考虑另一种分类办法也是有帮助的,即分为非本质复杂性和本质复杂性。

非本质复杂性(不完全列表)

1. 缺陷(指能够克服的问题,但解决方案很笨拙;C++的书里面把克服缺陷的workarounds称作技术,我觉得非常误导):例子在前面已经列了一堆了。

2. 陷阱(指无法克服的问题,只能小心绕过;如果跌进去,那就意味着你不知道这个陷阱,那么很大可能性你也不知道从哪去解决这个问题):一般来说,作为一个合格的程序员(不管是不是C++程序员),80%场景下的语言陷阱是需要记住才行的。比如深拷贝&浅拷贝;基类的析构函数应当为虚;缺省生成的类成员函数;求值顺序&序列点;类成员初始化顺序&声明顺序;导致不可移植代码的实现相关问题等。

本质复杂性(不完全列表)

1. 内存管理

2. 对象生命期

3. 重载决议

4. 名字查找

5. 模板参数推导规则

6. 异常

7. OO(动态)和GP(静态)两种范式的应用场景和交互

总而言之,这一节的目的是要告诉你从一个较高的层次去把握C++中的复杂性。其中最重要的一个指导思想就是在学习的过程中注意你正学习的技术或细节到底是80%场景下的还是20%场景下的(一般来说,读完两本书——后面会提到——之后你就能够很容易的对此进行判断了),如果是20%场景下的(有大量这类复杂性,其中尤数各种各样的workarounds为巨),那么也许最好的做法是只记住一个大概,不去作任何深究。此外,一般来说,不管使用哪门语言,认识语言陷阱对于编程来说都是一个必要的条件,语言陷阱的特点是如果你掉进去了,那么很大可能意味着你本来就不知道这有个陷阱,后者很大可能意味着你不知道如何解决。

posted @ 2008-05-07 23:55 董超 阅读(46) | 评论 (0)编辑

为了尝试一下python的正则表达式,我写了一个小程序命名为re.py,结果在运行的时候出了问题。根据错误提示到网上搜了一下也没有找到解决方法。最后还是自己明白过来了。原来re.py这个名字编译成字节码后是re.pyc,当在程序当中import re的时候,会首先在当前目录下寻找re.pyc,找到后就不会将/usr/local/lib/python2.5/里的re.pyc加载进来了。所以给源文件起名字应该避免和标准库重名。

但是python的库有很多,很难保证你起的名字就一定不会和它们中任何一个重名。所以我又写了个python程序来验证原文件名的合法性。
下面是源程序verify_py_name.py的代码:

#!/usr/bin/env python
import os, string, sys
path 
= '/usr/local/lib/python2.5/'
dic 
= {}
for root, dirs, files in os.walk(path):
        
for name in files:
                dic[string.split(name, 
'.'1)[0]] = None
for py_name in sys.argv[1:]:
        
if dic.has_key(string.split(py_name, '.'1)[0]):
                
print ':(    ' + py_name + ' is an illegal python source file name!'
        
else:
                
print '  :)  ' + py_name + ' is legal.'

使用方法:verify_py_name 参数1 [参数2 ...]
$ ./verify_py_name.py a.py re.py os.py
  :)  a.py is legal.
:(    re.py is an illegal python source file name!
:(    os.py is an illegal python source file name!

 

posted @ 2008-03-04 22:25 董超 阅读(48) | 评论 (0)编辑


函数性能测试头文件

debugging code

两个协助开发和测试的小工具,虽不见得有太大用处,却都是呕心沥血之作,07年也算对得住自己了。

2008年,我要把目光放开阔一些,C++,Python和C#都学一学,打算在这里写C++的学习笔记,我的笔记一定都是精华。

posted @ 2008-01-01 23:05 董超 阅读(90) | 评论 (0)编辑
  quote 'Expert C programming' written by Peter Van Der Linden

library call

system call

the C library is the same on every ANSI C implementation

the system calls are different in each OS

is a call to a routine in a library

is a call to the kernel for a service

linked with the user address space

is an entry point to the OS

executes in the user address space

executes in the kernel address space

counts as part of the “user” time

counts as part of the “system” time

has the lower overhead of a procedure call

has high overhead context switch to kernel and back

there are about 300 routines in the C library libc

There are about 90 system calls in Unix

Documented in Section 3 of the UNIX manual

Documented in Section 2 of the UNIX manual

typical C library calls: system, fprintf, malloc

typical system calls: chdir, fork, write, brk

posted @ 2007-10-07 11:04 董超 阅读(148) | 评论 (2)编辑
quote 'Expert C programming' written by Peter Van Der Linden

If a copy of the libraries is physically part of the executable, then we say the executable has been statically linked; if the executable merely contains filenames that enable the loader to find the program's library references at runtime, then we say it has been dynamically linked. The canonical names for the three phases of collecting modules together and preparing them for execution are linkediting, loading, and runtime linking. Statically linked modules are link edited and then loaded to run them. Dynamically linked modules are link-edited and then loaded and runtime-linked to run them. At execution, before main() is called, the runtime loader brings the shared data objects into the process address space. It doesn't resolve external function calls until the call is actually made, so there's no penalty to linking against a library that you may not call.

Although an individual executable has a slightly greater start-up cost, dynamic linking helps overall performance in two ways:
1. A dynamically linked executable is smaller than its statically linked counterpart. It saves disk and virtual memory, as libraries are only mapped in to the process when needed. Formerly, the only way to avoid binding a library copy into each executable was to put the service in the kernel instead of a library, contributing to the dreaded "kernel bloat."
2. All executables dynamically linked to a particular library share a single copy of the library at runtime. The kernel ensures that libraries mapped into memory are shared by all processes using them. This provides better I/O and swap space utilization and is sparing of physical memory, improving overall system throughput. If the executables were statically linked, each would wastefully contain its own complete duplicate copy of the library.

Dynamic linking permits easy versioning of libraries. New libraries can be shipped; once installed on the system, old programs automatically get the benefit of the new versions without needing to be relinked.

Finally (much less common, but still possible), dynamic linking allows users to select at runtime which library to execute against. It's possible to create library versions that are tuned for speed, or for memory efficiency, or that contain extra debugging information, and to allow the user to express a preference when execution takes place by substituting one library file for another.

Dynamic linking is "just-in-time" linking. It does mean that programs need to be able to find their libraries at runtime. The linker accomplishes this by putting library filenames or pathnames into the executable; and this in turn, means that libraries cannot be moved completely arbitrarily. If you linked your program against library /usr/lib/libthread.so, you cannot move the library to a different directory unless you specified it to the linker. Otherwise, the program will fail at runtime when it calls a function in the library, with an error message like:
ld.so.1: main: fatal: libthread.so: can't open file: errno=2

This is also an issue when you are executing on a different machine than the one on which you compiled. The execution machine must have all the libraries that you linked with, and must have them in the directories where you told the linker they would be. For the standard system libraries, this isn't a problem.

Static libraries are known as archives and they are created and updated by the ar—for archive—utility. The ar utility is misnamed; if truth in advertising applied to software, it would really be called something like glue_files_together or even static_library_updater.

Convention dictates that static libraries have a ".a" extension on their filename. There isn't an example of creating a static library here, because they are obsolete now, and we don't want to encourage anyone to communicate with the spirit world.

There was an interim kind of linking used in SVR3, midway between static linking and dynamic linking, known as "static shared libraries". Their addresses were fixed throughout their life, and thus could be bound to without the indirection required with dynamic linking. On the other hand, they were inflexible and required a lot of special support in the system. We won't consider them further.

A dynamically linked library is created by the link editor, ld. The conventional file extension for a dynamic library is ".so" meaning "shared object"—every program linked against this library shares the same one copy, in contrast to static linking, in which everyone is (wastefully) given their own copy of the contents of the library. In its simplest form, a dynamic library can be created by using the –G option to cc, like this:
% cat tomato.c
my_lib_function() {printf("library routine called\n"); }
% cc -o libfruit.so -G tomato.c
You can then write routines that use this library, and link with it in this manner:
% cat test.c
main() { my_lib_function(); }
% cc test.c -L/home/linden -R/home/linden -lfruit
% a.out
library routine called

The -L/home/linden -R/home/linden options tell the linker in which directories to look for libraries at linktime and at runtime, respectively.

You will probably also want to use the -K pic compiler option to produce position-independent code for your libraries. Position-independent code means that the generated code makes sure that every global data access is done through an extra indirection. This makes it easy to relocate the data simply by changing one value in the table of global offsets. Similarly, every function call is generated as a call through an indirect address in a procedure linkage table. The text can thus easily be relocated to anywhere, simply by fixing up the offset tables. So when the code is mapped in at runtime, the runtime linker can directly put it wherever there is room, and the code itself doesn't have to be changed.

By default, the compilers don't generate PICode as the additional pointer dereference is a fraction slower at runtime. However, if you don't use PICode, the generated code is tied to a fixed address— fine for an executable, but slower for a shared library, since every global reference now has to be fixed up at runtime by page modification, in turn making the page unshareable.

The runtime linker will fix up the page references anyway, but the task is greatly simplified with position-independent code. It is a trade-off whether PICode is slower or faster than letting the runtime linker fix up the code. A rule of thumb is to always use PICode for libraries.

Position-independent code is especially useful for shared libraries because each process that uses a shared library will generally map it at a different virtual address (though sharing one physical copy).

A related term is "pure code." A pure executable is one that contains only code (no static or initialized data). It is "pure" in the sense that it doesn't have to be modified to be executed by any specific process. It references its data off the stack or from another (impure) segment. A pure code segment can be shared. If you are generating PIcode (indicating sharing) you usually want it to be pure, too.

 

posted @ 2007-09-22 21:50 董超 阅读(237) | 评论 (0)编辑
WHAT A WONDERFUL CODE

(With apologies to George Weiss and Bob Thiele)


I can see the code, and change it too
I can make it do things I want it to do
And I think to myself, what a wonderful code

I see people around hacking the code
most of them only wears a bathrobe
And I think to myself, what a wonderful code

The functions is so pretty, the logic so clean
The source is free, GPL is not mean
Anyone who wants, can contribute
And if they want to, they can also distribute

The code is evolving, I watch it grow
I learn much more when I read the code
And I think to myself, what a wonderful code
Yes, I think to myself, what a wonderful code

oh yeah


by Peter Sundling.
posted @ 2007-09-07 23:52 董超 阅读(22) | 评论 (0)编辑