2008年10月22日星期三

我把事情搞砸了

事情得回溯到六月底,我一个人在康科办公室里,翻看着APUE的源码,我会很满足的告诉自
己:我在忙碌,我像一个有志向,发奋图强的年轻人一样在学习。但是面对着眼前的烂摊子
我就傻眼了————加拿大随时会发来稀奇古怪的需求,需要你改动一点点小玩意,在晚上下班
之前提交上去。

我讨厌了这样的工作,别人的软件让我在其之上修改一些小的界面,或者使用简单的汇编语
句取得一些硬件信息;完成之后我还要钻到实验室里,花费两天时间来测试效果。想起为了
完成新主板的MAPPING,我要投身两个星期来分析完芯片初期数据,那个经历真是糟糕透了。

我宁愿去看Linux内核,那是一个有创意的,有前途的东西,让我学习了一些硬件知识是如
何应用到操作系统设计之上的。后来我发现手头的工作极其枯燥,而且无法腾出时间来看内
核代码,我义无反顾的提交了辞呈。老板对于我的第二次辞呈没有发表太多意见,在一个月
后(七月底),我在一片静寂之中离开了付出两年汗水的公司。

八月初,我回到家乡,我见到了很多亲人老友,他们对我的经历一无所知,辞职的事情除了
老哥以外,我没有告诉任何人。家里有压在我心头的几块石头,完成这些琐碎之事以后,我
匆忙的回到了深圳,开始了漫长的求职时期。

我得说,我从没想过,求职期如此的漫长和烦躁,这让我生活的周边都显得更加煎熬。

我在八九月发出了超过30条求职信,而回复者寥寥无几。我认为这开始并不是什么稀奇事情,
很多人告诉我八到十月是招聘淡季,我对这些措辞不以为然,我不相信什么淡季,依据我的
能力,找到一个适合的工作没有大问题。现在回想起来,我对自己的自信慢慢感觉到羞愧!

九月份我的回复得到了稍许回应,我接到了几个公司的面试申请。于是我开始一一品尝面试
的滋味,腾讯公司,开源通讯公司,深信服公司,深圳傲天通讯,研祥科技大厦……这其中带
有趣味,然而更多的是苦痛。

在经历过如此多的面试之后,我得说,我根本不了解我工作的这一行是个什么样的状况,甚
至可以说,我根本没有对自己的工作做一个很好的评估————根本不知道自己要作什么。

想起毕业的时候我选择报一本《JAVA编程思想》南下找工作的情景,我觉得从理念上看,现
在和那个时候没有什么本质分别:我依然自信满满的认为我可以作自己要的工作,可对这一
份工作的概念都是模糊的。

我想作Linux程序员,但是来到人才招聘网上,我看到Linux程序员都有什么要求呢:
1) Linux驱动编程
2) Linux嵌入式开发
3) Linux后台开发

在这样的职位需求面前,我傻眼了,如果佯装是一个Linux程序员去面试职位,你几乎必须
面对以下问题之一:
1) Linux内核裁减
2) Linux驱动程序编写和移植
3) 嵌入式CPU架构和编程环境
4) Linux网络编程

如果你没有以上经验,那么你几乎没有和人事部经理谈话的资格,在技术面试官前就会被淘
汰。可是我就没有,全部NO,如我所说,80%的面试官都不会接受我的。

剩下20%是通过面试题来测试的,我得说,我是一个应试型的人才,面对考试不以为然。但
是在答复一些笔试题时候,我仍然深受打击————我的专业功底显得那么的不堪一击!我经历
过的面试题浩如烟海,他们覆盖了计算机知识绝大多数部分:
1. C/C++语言功底,C++庞大的语言特性,Shell高级编程
2. UNIX环境编程,Socket编程,多线程编程
3. UNIX网络编程,tcp/ip,http协议
4. 数据结构和算法设计,一些查找和排序算法
5. CPU架构,x96, Power, MIPS架构知识

要从理论上学习这些,我起码要看完手头的大堆书籍。他们包括:
Thinking in C++
Effective C++
UNIX环境编程 APUE
UNIX网络编程 UNP
编程艺术
算法设计
Linux设备驱动
深入理解Linux内核
... ...

顿时我觉得自己非常渺小,经过的两年工作时间在眼前化为乌有。

如果没有什么奇迹发生的话,我仍将领教上面所说的面试和笔试问题,我将再次面临自己薄
弱的知识和经验积累,我还会在离职的深渊中不断轮回。因此这样一个明显的选择摆在我的
面前:要么尝试其他低阶的工作甚至换行,要么就要费劲心力来补齐自己的不足。更要命的
是,我花费这么长的时间才发觉这个问题,那么离职时我在做什么呢?一年前呢?哦~这让
我实在难受————

天呐!我把事情搞砸了!

[LDD] C3. Character Device driver

代码阅读 <Linux Device Driver> v3
Chapter 3. Character Device Driver

一、scull介绍 Simple Chracter Utility for Loading Localities
scull是一个操作内存区域的字符设备。

scull源码实现的设备有:
scull0 ~ scull3
这四个设备分别由一个全局且持久的内存区域组成。
scullpipe0 ~ scullpipe3
这四个FIFO与管道类似。一个进程读取由另一个进程写入的数据。如果多个进程读
取同一个设备,它们就会为数据发生竞争。scullpipe的内部实现将说明在不借助
于中断的情况下如何实现阻塞式和非阻塞式读写操作。
scullsingle
scullpriv
sculluid
scullwuid
这些设备与scull0相似,但在何时允许open操作方面有一些限制。
scullsingle,一次只允许一个进程使用该驱动程序。
scullpriv,它对每个虚拟控制台是私有的。因为控制台进程会获取不痛的内存区。
sculluid和scullwuid可以被多次打开,但是每次只能由一个用户打开;如果另一
个用户锁定该设备,sculluid则返回“Device Busy”错误,而scullwuid则实现了阻
塞式open。

二、设备编号
对字符设备的访问是通过文件系统内的设备文件进行的,这些设备文件通常位于/dev。而这
些文件包含两个重要数据,主设备号和次设备号。

主设备号表示对应的驱动程序,现代的Linux允许多个驱动程序共享主设备号,但一般仍旧
遵循“一个主设备号对应一个驱动程序”的原则。

次设备号由内核使用,用于正确确定设备文件指定的设备。

MAJOR(dev_t dev); /* 返回主设备号 */
MINOR(dev_t dev); /* 返回次设备号 */
MKDEV(int major, int minor); /* 由主次设备得到dev_t类型的设备编号 */


关于分配和释放设备编号,系统在linux/fs.h中声明了以下函数:

/* 获得一个或者多个设备编号 */
int register_chrdev_region(dev_t first, unsigned int, char *name);

/* 运行过程中,使用以下函数会为我们恰当分配所需要的设备号 */
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
unsigned int count, char *name);

/* 应该用以下函数释放这些设备编号 */
void unregister_chrdev_region(dev_t first, unsigned int count);


三、重要数据结构

#include <linux/fs.h>
struct inode {
struct hlist_node i_hash;
struct list_head i_list;
struct list_head i_sb_list;
struct list_head i_dentry;
unsigned long i_ino;
atomic_t i_count;
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev;
u64 i_version;
loff_t i_size;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
unsigned int i_blkbits;
blkcnt_t i_blocks;
unsigned short i_bytes;
umode_t i_mode;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
struct mutex i_mutex;
struct rw_semaphore i_alloc_sem;
const struct inode_operations *i_op;
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct super_block *i_sb;
struct file_lock *i_flock;
struct address_space *i_mapping;
struct address_space i_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
/* ... */
};

struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
atomic_t f_count;
unsigned int f_flags;
mode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

/* ... */
};

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};

/* 自定义结构 */
struct scull_qset {
void **data;
struct scull_qset *next;
};

struct scull_dev {
struct scull_qset *data;
int quantum;
int qset;
unsigned long size;
unsigned int access_key;
struct semaphore sem;
struct cdev cdev;
};


struct inode ————
内核使用inode结构在内部表示文件,因此它和file结构不同,后者表示打开的文件描述符。
对单个文件,可能会有许多个表示打开的文件描述符的file结构,但他们都指向单个inode
结构。

inode包含大量节点文件信息。而此驱动只是用到了以下两个字段:
dev_t i_rdev; // 对于表示设备的inode结构,该字段包含真正的设备编号
struct cdev *i_cdev; // 对于表示字符设备的inode结构,此字段包含struct cdev指针

另外,开发者可以通过两个宏来获取主次设备编号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

struct file ————
file结构代表一个打开的文件,系统中任何一个打开的文件都有一个对应的file结构,他由
内核在open时候创建,并传递给在文件上的所有函数,知道最后的close函数。在文件实例
关闭之后,内核会释放这个结构。用到的字段有:

struct path f_path;
#define f_dentry f_path.dentry // 目录项结构
const struct file_operations *f_op;
unsigned int f_flags; // 标志,是否阻塞
mode_t f_mode; // 权限检查
loff_t f_pos; // 当前读写位置
/* needed for tty driver, and maybe others */
void *private_data; // 跨系统调用时候保存状态信息


struct file_operations ————
file_operations用来建立驱动程序操作和驱动设备编号之间的连接,他包含了一组函数指
针,这些函数和某些系统调用相关联。程序员可以视此结构为面向对象的C编程的一个例证。

file_operations的函数指针必须指向驱动程序实现特定操作的函数,对于不支持的操作,
对应的字段被赋值NULL,这种情况下,内核的处理行为是不尽相同的。
在本例中,我们实现了以下操作:

struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};


四、scull内存使用
两个函数 ————
void *kmalloc(size_t size, gfp_t flags);
void kfree(void *ptr);
这个两个函数定义在linux/stab.h中,是内核中非常重要的两个内存操作函数。用kmalloc
函数分配,然后用kfree释放,同时也注意不应该把非kmalloc返回的指针传递给kfree。
(但是将NULL传递给kfree是合法的)


scull驱动中,每一个scull设备(scull_dev)都是一个链表头,他的指针指向scull_qset
结构的链表。而每一个scull_qset则是典型的链表结构,包含了一个4000bytes * 1000的内
存区域,这里的每一个区域就被称为是一个量子(quantum)。
4000和1000的尺寸,由宏SCULL_QUANTUM和SCULL_QSET定义。当然,你也可以设置为模块参
数,也可以在运行时使用ioctl来更改。

由于scull设备是虚拟的内存区域,所以我们的read/write操作也将是对内存的操作,顶多
的麻烦是跨越内核空间和用户空间的拷贝而已。这让实现变的非常容易。

五、实现
初始化和清理函数

int scull_init_module(void)
{
int result, i;
dev_t dev = 0;

if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
"scull");
scull_major = MAJOR(dev);
}

if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}

/* allocate the devices -- we can't have them static, as the number
* can be specified at load time.
*/
scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_devices) {
result = -ENOMEM;
goto fail;
}
memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

/* initialize each device */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
init_MUTEX(&scull_devices[i].sem);
scull_setup_cdev(&scull_devices[i], i);
}

/* At this point call the init function for any friend device */
dev = MKDEV(scull_major, scull_minor + scull_nr_devs);
// dev += scull_p_init(dev);
// dev += scull_access_init(dev);

#ifdef SCULL_DEBUG /* only when debugging */
// scull_create_proc();
#endif
printk(KERN_INFO "init scull successful\n");
return 0; /* succeed */

fail:
scull_cleanup_module();
return result;
}

void scull_cleanup_module(void)
{
int i;
dev_t devno = MKDEV(scull_major, scull_minor);

/* Get rid of our char dev entries */
if (scull_devices) {
for (i = 0; i < scull_nr_devs; i++) {
scull_trim(scull_devices + i);
cdev_del(&scull_devices[i].cdev);
}
kfree(scull_devices);
}

#ifdef SCULL_DEBUG /* use proc only if debugging */
scull_remove_proc();
#endif

/* cleanup_module is never called if registering failed */
unregister_chrdev_region(devno, scull_nr_devs);

/* and call the cleanup functions for friend devices */
// scull_p_cleanup();
// scull_access_cleanup();
printk(KERN_INFO "cleanup scull successful\n");
}

init_module过程包括 ————
1) 注册设备
2) 初始化scull_dev设备
3) 初始化其他设备
为了简化设计,此章节没有引入pipe等设备,所以注销了某些方法。

cleanup_module过程 ————
1)释放scull_dev结构,注销字符设备
2) 注销设备号


然后是在file_operations指明的函数,这里重点讲解read和write的实现 ————

ssize_t scull_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if(*f_pos >= dev->size)
goto out;
if(*f_pos + count > dev->size)
count = dev->size - *f_pos;

item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

dptr = scull_follow(dev, item);

if (dptr == NULL || !dptr->data || !dptr->data[s_pos])
goto out;

if (count > quantum - q_pos)
count = quantum - q_pos;

if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
out:
up(&dev->sem);
return(retval);
}


ssize_t scull_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if(*f_pos >= dev->size)
goto out;
if(*f_pos + count > dev->size)
count = dev->size - *f_pos;

item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

dptr = scull_follow(dev, item);

if (dptr == NULL)
goto out;
if (!dptr->data) {
dptr->data = kmalloc(qset *sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}

if (count > quantum - q_pos)
count = quantum - q_pos;

if (copy_from_user(dptr->data[s_pos] + q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

/* 更新文件大小 */
if (dev->size < *f_pos)
dev->size = *f_pos;
out:
up(&dev->sem);
return(retval);
}


scull_read从filp的f_pos处读取count个字节,写到buf中。
首先要作的是,确定f_pos的位置,根据f_pos,我们要确定写的位置,位于第几个item的
第s_pos数组的第q_pos个量子处,然后根据item返回我们要的scull_qset结构体。最后使用
copy_to_user完成读操作。

写操作和读操作很相似,使用了逆向的copy_from_user函数。

这两个重要的系统调用实现,都使用了跨越内核和用户空间的写操作:
copy_to_user(void *to, void *from, int count);
copy_from_user(void *to, void *from, int count);

六、总结
scull_init_module
* alloc_chrdev_region
* scull_setup_cdev
** cdev_init, cdev_add
** scull_fops(llseek, read, write, ioctl, open, release)
* scull_create_proc
* scull_p_init
* scull_access_init

_

2008年10月14日星期二

[TIC++] C13. Dynamic Object Creation

代码阅读<Thinking In C++>
Chapter 13. Dynamic Object Creation

一、在堆区创建内存

// c13: malloc_class.cpp
// malloc with class objects
#include <cstdlib>
#include <cstring>
#include <cstring>
#include <iostream>
using namespace std;

class obj {
int i, j, k;
enum { sz = 100 };
char buf[sz];
public:
void initialization() { // can't use constructors
cout << "initialization obj" << endl;
i = j = k = 0;
memset(buf, 0, sz);
}
void destroy() {
cout << "destroy obj" << endl;
}
};

int main()
{
obj *o = (obj*)malloc(sizeof(obj));
if (o == 0)
return -1;
o->initialization();
//sometimes later...
o->destroy();
free(o);
}
/* result:
initialization obj
destroy obj
*/

当C++对象创建的时候,将会发生两件事情:
1) 为空间分配内存
2) 调用构造方法来初始化这块内存

而空间创建问题(1),可以有以下选择:
1) 在程序开始前创建静态空间,位于静态内存区,拥有文件生命期
2) 在执行点处创建堆栈空间,他将在执行跳出方法的时候释放。堆栈的处理是处理器完成
的,自然非常高效
3) 在执行点处创建堆空间(heap),这被成为动态内存分配(!)。方法必须在运行调用的时候
创建堆空间,这意味着你可以决定任何时候创建,当然也要为释放负全权责任————生命期完
全由程序员掌控。

上面的例子展示了C处理堆区空间的方法:
这里的obj o对象的创建,没有使用到构造方法!这是非常糟糕的情况,因为你有可能选择
是否初始化,甚至可能忘记,初始化的丢失极为可能带来bug。

于是C++引入了一种新方法:

// c13: new_and_delete.cpp
#include <iostream>
using namespace std;

class tree {
int height;
public:
tree(int tree_height): height(tree_height) {}
~tree() { cout << "*"; }
friend ostream& operator <<(ostream &os, const tree* t) {
return os << "tree height is: "
<< t->height << std::endl;
}
};

int main(void)
{
tree *t = new tree(40);
cout << t;
delete t;
}
/* result
tree height is: 40
*/

new和delete保证了构造方法的正常调用,甚至还会检查内存申请是否调用成功。
这一切显得像堆栈内存分配一样简单。

二、delete void*

// c13: bad_void_pointer_deletion.cpp
#include <iostream>
using namespace std;

class object {
void *data;
const int size;
const char id;
public:
object(int sz, char c): size(sz), id(c) {
data = new char[size];
cout << "constructing object" << id
<< ", size = " << size << endl;
}
~object() {
cout << "desctructing object " << id << endl;
delete []data; // OK, just release storage;
// no desctructor calls are necessary;
}
};

int main() {
object *a = new object(40, 'a');
delete a;
void *b = new object(40, 'b');
delete b;
}
/* result:
constructing objecta, size = 40
desctructing object a
constructing objectb, size = 40
*/

object是一个包含 void *data的一个类,在这个类的析构方法中,我们看到使用了delete
来析构data,这没什么问题,因为这里要作的只是释放内存。

但是在main()中,对于delete来说,对象类型信息就非常必要了。由于delete a知道a是一
个class object,所以他使用到了~object()析构方法,data也被释放了。可是void *b就遇
到麻烦,他无法得知void*的类型信息,就无法调用析构方法,因此data将会永远的丢失,
这是一个静静的内存泄露。

如果你在C++工程中有内存泄露问题,那就要好好检查你的delete了。

三、介绍new handler

// c13: new_handler.cpp
#include <iostream>
#include <cstdlib>
#include <new>
using namespace std;

int count = 0;

void out_of_memory()
{
cerr << "memory exhausted after " << count << " allocations" << endl;
exit(1);
}

int main()
{
set_new_handler(out_of_memory);
while(1) {
count++;
new int[1000];
}
}

当new无法找到一个足够大的堆空间来存放对象,会出现何种情况呢?
这样的情况下,就会调用new-handler。默认的new-handler会抛出异常,当然可以定义新的
new-handler,打印出信息告诉你发生了什么。

这里就是用<new>的set_new_handler设置了方法,他会打印在经历多少次存储之后耗尽内存。

_

[TIC++] C11. References & the Copy-Constructor

代码阅读<Thinking In C++>
Chapter 11. References & the Copy-Constructor

一、再次介绍引用

// reference.cpp
int *f(int *x)
{
(*x)++;
return x;
}

int &g(int &x)
{
x++;
return x;
}

int &h()
{
int q;
//! return q; // error
static int x;
return x;
}

int main()
{
int a = 0;
f(&a);
g(a);
}

reference可以被简单视作一个常量指针,而这个指针会在预编译期间自动被解释掉。
ref最常用到的地方,就是作为参数和返回值使用了。对参数而言,任何形参的改变会作用
到实参;针对返回值,要注意的是返回的东西只是引用,必须保证要返回的内存在返回地可
见。
* 因此,你不能在h()中返回局部变量。

在介绍copy constructor之前,让我们来看常引用情况 ————

// const_ref.cpp
void f(int&) {}
void g(const int&) {}

int main() {
//! f(1); //invalid initialization of non-const reference
g(1);
}

当传递1这个实参的时候,编译器会为1分配内存,然后创造一个引用(int&)绑定到这个内存
地址。如果1没有被声明为const,那将是毫无意义的。f(1)将得到一个编译时错误。

最后是复合化的引用,指向指针的引用 ————

// c11: ref_to_pointer.cpp
#include <iostream>
using namespace std;

void increment(int* &i) { i++; }

int main()
{
int *i = 0;

cout << "i = " << i << endl;
increment(i);
cout << "i = " << i << endl;
}

这里阵阵改变的是指针(int*)i,而不是指针指向的值,但是这里传递的i。
在C中要做到这点,那就要使用二级指针了。

二、copy-constructor

// c11: howmany.cpp
#include <fstream>
#include <string>

using namespace std;
ofstream out("howmany.out");

class howmany {
static int object_cnt;
public:
howmany() { object_cnt++; }
static void print(const string& msg = "") {
if (msg.size() != 0)
out << msg << ": ";
out << "object_cnt = " << object_cnt << endl;
}
~howmany() {
object_cnt--;
print("~howmany()");
}
};

int howmany::object_cnt = 0;

// pass and return by value:
howmany f(howmany x)
{
x.print("x argument inside f()");
return x;
}

int main()
{
howmany h;
howmany::print("after construction of h");
howmany h2 = f(h);
howmany::print("after call to f()");
}
/* result:
after construction of h: object_cnt = 1
x argument inside f(): object_cnt = 1
~howmany(): object_cnt = 0
after call to f(): object_cnt = 0
~howmany(): object_cnt = -1
~howmany(): object_cnt = -2
*/

首先我们构建howmany h,调用构造函数,会得到object_cnt = 1;
然后我们使用f()来构建第二个对象h2 = f(h),编译器传递h拷贝,并没有使用构造函数来
创建h2。但是意想不到的是,离开f()的时候发生了析构,导致object_cnt = 0。
而最后的两个析构,使得object_cnt得到负数。

这是为什么呢?
因为在f()内,函数使用了C形式的bitcopy,我们得到的只是一份h的拷贝,他没有调用构造
方法,所以我们没有看到f()中objecgt_cnt = 2。而在后来,C++特性去保证了最后一步析
构,因此出现了object_cnt不升反降的结果。

C++中为了保证构造的完整性,因此提出了copy-constructor————

// c11: howmany.cpp
#include <fstream>
#include <string>
using namespace std;

ofstream out("howmany2.out");

class howmany2 {
string name;
static int object_cnt;
public:
howmany2(const string &id = ""): name(id) {
++object_cnt;
print("howmany2()");
}
~howmany2() {
--object_cnt;
print("~howmany()");
}
howmany2(const howmany2 &h): name(h.name) {
name += " copy";
++object_cnt;
print("howmany2(const howmany2&)");
}
void print(const string& msg = "") const {
if (msg.size() != 0)
out << msg << endl;
out << '\t' << name << ": "
<< "object_cnt = " << object_cnt << endl;
}
};

int howmany2::object_cnt = 0;

// pass and return by value:
howmany2 f(howmany2 x)
{
x.print("x argument inside f()");
return x;
}

int main()
{
howmany2 h("h");
out << "entering f()" << endl;
howmany2 h2 = f(h);
h2.print("h2 after call to f()");
out << "call f(), no return value" << endl;
f(h);
out << "after call to f()" << endl;
}
/*
howmany2()
h: object_cnt = 1
entering f()
howmany2(const howmany2&)
h copy: object_cnt = 2
x argument inside f()
h copy: object_cnt = 2
howmany2(const howmany2&)
h copy copy: object_cnt = 3
~howmany()
h copy: object_cnt = 2
h2 after call to f()
h copy copy: object_cnt = 2
call f(), no return value
howmany2(const howmany2&)
h copy: object_cnt = 3
x argument inside f()
h copy: object_cnt = 3
howmany2(const howmany2&)
h copy copy: object_cnt = 4
~howmany()
h copy copy: object_cnt = 3
~howmany()
h copy: object_cnt = 2
after call to f()
~howmany()
h copy copy: object_cnt = 1
~howmany()
h: object_cnt = 0
*/

我们可以看到,f()传递参数的时候,编译器将会把拷贝传值的过程交给copy-constructor,
同样return返回值,也需要这样的拷贝。
entering f()
howmany2(const howmany2&)
h copy: object_cnt = 2
x argument inside f()
h copy: object_cnt = 2
howmany2(const howmany2&)
h copy copy: object_cnt = 3

而后离开f(),这需要析构刚才创建的临时变量,保留return的值。

第二次f()没有返回值,因此情况稍微发生改变。此种调用忽略了返回值,编译器也会聪明
的直接在调用点析构此临时对象。

三、选择copy-construction
我们来看如何迫使系统自建拷贝构造方法

// c11: default_cc.cpp
// automatic creation of the copy-constructor
#include <iostream>
#include <string>
using namespace std;

class withcc {
public:
withcc() {}
withcc(const withcc &) {
cout << "withcc(withcc &)" << endl;
}
};

class wocc {
string id;
public:
wocc(const string &ident = ""): id(ident) {}
void print(const string &msg = "") const {
if (msg.size() != 0)
cout << msg << ": ";
cout << id << endl;
}
};

class composite {
withcc tc;
wocc oc;
public:
composite(): oc("composite()") {}
void print(const string &msg = "") const {
oc.print(msg);
}
};

int main() {
composite c;
c.print("content of c");
cout << "calling composite copy-constructor" << endl;
composite c2 = c;
c2.print("content of c2");
}
/* reuslt
content of c: composite()
calling composite copy-constructor
withcc(withcc &)
content of c2: composite()
*/

我们在这里看到两个案例:
1) class withcc拥有构造方法和拷贝构造方法。一旦包含拷贝构造函数,这就是在告诉编
译器:永远不要再自作主张,自动创建默认构造函数。因此,如果class withcc没有定义
withcc(),编译器会报错——composite中的tc没有办法创建。

2) class composite拥有构造方法,而没有拷贝构造方法。一旦需要拷贝构造方法,编译器
会为所有的成员对象调用他们的拷贝构造方法。(这通过组合或者继承可以实现composite)
在这里,c2 = c,就强迫调用了withcc和wocc的拷贝构造方法。而wocc没有拷贝构造方法,
因此系统为其自动定义一个,完成bitcopy。

接下来,通过小技巧来避免赋值拷贝

// c10: no_cc.cpp
class nocc {
int i;
nocc(const nocc &);
public:
nocc(int ii = 0): i(ii) {}
};

void f(nocc);

int main(void)
{
nocc n;
// copy-constructor is private!
//! f(n);
//! nocc n2 = n;
//! nocc n3(n);
}

通过将拷贝构造方法设置为私有,来避免赋值拷贝,这是一个非常简单的方法。
在此不用赘述。
_

2008年10月11日星期六

[TIC++] C10. Name Control

代码阅读<Thinking In C++>
Chapter 10. Name Control

一、局部变量作为static

// c10: static_objects_inside_funcs.cpp
#include <iostream>
using namespace std;

class X {
int i;
public:
X(int ii = 0):i(ii) {}
~X() { cout << "X::~X()" << endl; }
};

void f()
{
static X x1(47);
static X x2;
}

int main()
{
f();
} ///:~
/* result:
X::~X()
X::~X()
*/

当局部变量声明为static的时候,此变量将不会放置在堆栈区内,而是放置在程序的静态内
存区。这就意味着static变量:
* 具有全局生命期
* 初始化只进行一次
* 此变量在声明函数外部不可见

来解析一下静态对象的析构方法:

// c10: static_destructors.cpp
// static object destructors

#include <fstream>
using namespace std;

ofstream out("statdest.out");

class obj {
char c;
public:
obj(char cc): c(cc) {
out << "obj::obj() for " << c << endl;
}
~obj() {
out << "obj::~obj() for " << c << endl;
}
};

obj a('a');

void f()
{
static obj b('b');
}

void g()
{
static obj c('c');
}

int main()
{
out << "inside main()" << endl;
f();
//g() not called
out << "leaving main()" << endl;
}
/* statdest.out:
obj::obj() for a
inside main()
obj::obj() for b
leaving main()
obj::~obj() for b
obj::~obj() for a
*/

静态变量具有全局生命期,这样静态对象会在main()函数退出时候,或者调用C标准函数
exit()的时候被自动调用析构方法。这意味着:如果你在静态对象的析构方法内exit,就会
造成一个死循环这是很危险的。另外,调用abort()函数结束程序,将不会自动调用静态
对象的析构方法。

这个例子解释了一些基本概况,a是全局对象,b是f()内部的静态对象,他们的构造和析构
顺序正好相反:
cons a -> cons b -> des b --> des a
(在C++中,全局静态对象的构造函数是在main()之前被调用的)

二、全局变量为static
local_extern.cpp -- 第一个模块(main)

// c10: local_extern.cpp

#include <iostream>

int main()
{
extern int i;
std::cout << i << endl;;
}
/* result:
5
*/

local_extern2.cpp -- 第二个模块

// c10: local_extern2.cpp
int i = 5;

static 和 extern在 C++中是一对反义词。
* 默认全局变量是extern的,extern关键词标明次变量包含外部链接,可以在其他模块中引
用。而static则约束此变量为内部链接,不可外部引用。这对于“非成员”函数也同样适用。
file scope:
1) 全局变量声明默认为extern,而声明为static只会更改可视范围,这样变量只包含内部
链接————它仍旧会位于静态数据区域,无论static还是extern。

2) 局部变量声明默认为auto,若声明为static则会更改可视范围,并且更改存储区域(从
堆栈区更改到静态数据区);若声明为extern,则显示此变量位于别的模块。

3) 方法定义为static/extern,只会改变可视范围。

三、名字空间 namespaces

// c10: continuation.cpp
namespace mylib {
extern int x;
void f();
}

namespace mylib {
extern int y;
void g();
}

int main() {}

* 名字空间的定义必须在全局区内,或者被嵌套在其他名字空间内
* 定义的末尾没有";"
* 可以在多个头文件里添加名字空间定义(原本看起来像重复定义)
* 名字空间可以被引用为其他名字,比如
namespace bob = bobs_super_duper_library;
* 对名字空间内部数据的引用类似于成员操作
* 你不能使用名字空间建立实例,他根本不是一个类型

四、using 指令
namespace_int.h -- 定义名字空间Int

// c10: namespace_int.h
#ifndef __NAMESPACEINT_H
#define __NAMESPACEINT_H

namespace Int {
enum sign { positive, negative };
class Integer {
int i;
sign s;
public:
Integer(int ii = 0): i(ii),
s(i >= 0 ? positive : negative)
{}
sign get_sign() const { return s; }
void set_sign(sign sgn) { s = sgn; }
}
}

#endif /// __NAMESPACEINT_H

namespace_math.h -- 定义名字空间Math

// c10: namespace_math.h
#ifndef __NAMESPACEMATH_H
#define __NAMESPACEMATH_H

#include "namespace_int.h"

namespace Math {
using namespace int;
Integer a, b;
Integer divide(Integer, Integer);
}

#endif /// __NAMESPACEMATH_H

arithmetic.cpp

// c10: arithmetic.cpp
#include "namespace_math.h"
void arithmetic()
{
using namespace Int;
Integer x;
x.set_sign(positive);
}

int main() {}

在arithmetic()的方法中,我们看到了using指令,如果没有他,任何命名空间的内容都需
要全名提供。

五、C++中的static数据成员

// c10: static_init.cpp
#include <iostream>
using namespace std;

int x = 100;

class with_static {
static int x;
static int y;
public:
void print() const {
cout << "with_static::x = " << x << endl;
cout << "with_static::y = " << y << endl;
}
};

int with_static::x = 1;
int with_static::y = x + 1;
// with_static::x not ::x

int main()
{
with_static ws;
ws.print();
}
/*
with_static::x = 1
with_static::y = 2
*/

类的static成员存在于一个独立的空间中,无论建立多少个对象,他们的static成员都是共
享的。更重要的是static成员只有在类对象中可见,它也可以受成员访问符约束。

在此例子中,在没有创建实例的时候,我们就对with_static::x进行初始化,这对于静态成
员是可行的。


// c10: static_array.cpp
class values {
static const int scsize;// = 100;
static const long sclong = 100;

static const int scints[];
static const long sclongs[];
static const float sctable[];
static const char scletters[];
static int size;
static const float scfloat; // differ from tic++
static float table[];
static char letters[];
};
int values::scsize = 100;
int values::size = 100;
const float values::scfloat = 1.1;
const int values::scints[] = { 99, 47, 33, 11, 7 };
const long values::sclongs[] = { 99, 47, 33, 11, 7 };
const float values::sctable[] = { 1.1, 2.2, 3.3, 4.4 };
const char values::scletters[] = { 'a','b','c','d','e','f','g','h','i','j',};
float values::table[4] = { 1.1, 2.2, 3.3, 4.4 };
char values::letters[10] = { 'a','b','c','d','e','f','g','h','i','j',};

int main() { values v; }

这里揭示了static初始化的可行性分析:
1) static 不可以在类中初始化,必须要求const。
2) static const 非原子类型,不可以在类中初始化。
3) static const 原子类型,可以而且必须在类中初始化。

下面来看嵌套类定义和局部类定义中的static情况————

// c10: local.cpp
// static member & local classes
#include <iostream>
using namespace std;

// Nested classes CAN have static data members;
class outer {
class inner {
static int i;
};
};

int outer::inner::i = 47;

//local class can not have static data members;
void f()
{
class local {
public:
//! static int i; // error
// (how could you define i?)
} x;
}

int main()
{
outer x;
f();
}

你可以发现嵌套类定义中的static完全可行,但是在局部类定义中却被禁止了。
我们怎么办呢?实际上,局部类定义是很少使用的。

六、static成员方法

// c10: static_member_funcs.cpp
class X {
int i;
static int j;
public:
X(int ii = 0): i(ii) {
// non-static member function can
// access static member function or data
j = i;
}
int val() const { return i; }
static int incr() {
//! i++; // Error: static member function
// cannot access non-static member data
return ++j;
}
static int f() {
//! val(); // Error: static member function
// cannot access non-static member function
return incr();
}
};

int X::j = 0;

int main()
{
X x;
X *xp = &x;

x.f();
xp->f();
X::f(); // only works with static members
} ///:~

同样,我们在C++中可以像定义成员数据那样定义一个static成员方法,在我们需要创建一
个仅仅作用于类或者对象的方法的时候,我们不希望他在全局区域内造成命名污染,因此,
这个念头就诞生了。

static方法可以直接被引用调用,不需要建立类实例(就像数据成员初始化一样)。有一点
要注意的是:static成员方法,他并不包含this指针,这注定了它:

* 不能够访问数据成员,也不能调用成员方法。


// c10: singleton.cpp
#include <iostream>
using namespace std;

class egg {
static egg e;
int i;

egg(int ii): i(ii) {}
egg(const egg&); // prevent copy-construction
public:
static egg* instance() { return &e; }
int val() const { return i; }
};

egg egg::e(47);

int main()
{
//! egg x(1); // private constructor
cout << egg::instance()->val() << endl;
}

这个例子非常有趣,除了egg::e,你将永远无法创造第二个egg对象。

1) 因为egg的构造函数设置为私有,你无法直接创建私有对象。
2) 使用static成员方法egg::instance(),你可以创建一个实例,这个实例是
static egg e;
3) 由于egg e本身也是e,那么所有的 instance()调用,都会返回这一个对象。其他对象创
建不能。
4) 此例还禁用了拷贝构造方法 egg(const egg&),这意味着,你甚至无法赋值对象,最后
一条路也被封锁了。

七、static 初始化依赖 static initialization dependency
initializer.h

// c10: initializer.h
#ifndef __INITIALIZER_H
#define __INITIALIZER_H

#include <iostream>

extern int x;
extern int y;

class initializer {
static int init_count;
public:
initializer() {
std::cout << "initializer()" << std::endl;
//initialize first time only
if (init_count++ == 0) {
std::cout << "performing initialization"
<< std::endl;
x = 100;
y = 200;
}
}
~initializer() {
std::cout << "initializer()" << std::endl;
//cleanup last time only
if (--init_count == 0) {
std::cout << "performing cleanup"
<< std::endl;
// any necessary cleanup here
}
}
};

static initializer init;

#endif//__INITIALIZER_H

initializer_defs.cpp -- 已知模块

// c10: initializer_defs.cpp
#include "initializer.h"

int x;
int y;
int initializer::init_count;

initializer.cpp -- 测试模块(main)

// c10: initializer.cpp
#include "initializer.h"
using namespace std;

int main()
{
cout << "inside main()" << endl;
cout << "leaving main()" << endl;
}
/* result:
initializer()
performing initialization
initializer()
inside main()
leaving main()
initializer()
initializer()
performing cleanup
*/

在extern的初始化过程中,我们会遇到这样的问题:

extern std::ofstream out;
class Oof {
public:
Oof() { std::out << "ouch"; }
} Oof;

这段代码引用到了其他文件的一个对象ofstream out,如果两个文件位于不同的模块,那么
这意味着out的定义和使用位于两个模块。在编译器编译的时候,往往不能完全决定哪一个
模块先编译,操作系统也没有提供确保初始化顺序的绝对方法。如果是此段代码先编译,那
么面对的问题就是,out还未构造就使用了,这会带来混乱。
这个问题针对的对象是(它们会在main()之前执行初始化):
1) 全局对象
2) 局部static对象

为了解决此问题的发生,我们给出三种方法
1) 永远别这样做,避免此类问题发生是最好的选择
2) 如果必须这样做,把静态对象定义放在一个文件里,这样你可以安排预想的编译次序。
3) 如果不可避免的要把静态对象放在不同模块中,可以采用两种技巧来解决此问题。

本段例子就是技巧1,它是Jerry Schwarz在创建iostream库的时候提出的。
initializer.h *设计目标
initializer_defs.cpp *已知模块
initializer.cpp *已知模块
我们要求的目标是,无论initializer_defs.cpp和initializer.cpp哪一个先初始化,都可
以保证x,y的安全使用。
实现方法:首先在initializer.h中,extern引用x,y,然后定义为x,y专门的初始化类
initializer,根据static成员int_count是否为0,判断x,y是否已经被初始化,如果未初始
化,则实现初始化;否则不要动。以同样的方法定义析构方法。接着在文件末尾添加static
对象initializer init。
最后一步,在x,y对象定义的模块初始化 initializer::init_count = 0;

由于已知模块都已经包含了initializer.h头文件,那么他们将共同拥有init对象,init对
象保证了对象x,y的初始化;由于class initializer的处理,也将屏蔽x,y的重复初始化。

下面介绍技巧2————
dependency1.h

// c10: dependency1.h
#ifndef __DEPENDENCY1_H
#define __DEPENDENCY1_H

#include <iostream>

class dependency1 {
bool init;
public:
dependency1(): init(true) {
std::cout << "dependency1 construction"
<< std::endl;
}
void print() const {
std::cout << "dependency1 init:"
<< init << std::endl;
}
};

#endif//__DEPENDENCY1_H

dependency2.h

// c10: dependency2.h
#ifndef __DEPENDENCY2_H
#define __DEPENDENCY2_H

#include "dependency1.h"

class dependency2 {
dependency1 d1;
public:
dependency2(const dependency1 &dep1): d1(dep1) {
std::cout << "dependency2 construction ";
print();
}
void print() const { d1.print(); }
};

#endif//__DEPENDENCY2_H

dependency1statfun.h

// c10: dependency1statfun.h
#ifndef __DEPENDENCY1STATFUN_H
#define __DEPENDENCY1STATFUN_H

#include "dependency1.h"
extern dependency1 &d1();

#endif//__DEPENDENCY1STATFUN_H

dependency2statfun.h

// c10: dependency2statfun.h
#ifndef __DEPENDENCY2STATFUN_H
#define __DEPENDENCY2STATFUN_H

#include "dependency2.h"
extern dependency2 &d2();

#endif//__DEPENDENCY2STATFUN_H

dependency1statfun.cpp

// c10: dependency1statfun.cpp
#include "dependency1statfun.h"

dependency1 &d1()
{
static dependency1 dep1;
return(dep1);
}///:~

dependency2statfun.cpp

// c10: dependency2statfun.cpp
#include "dependency1statfun.h"
#include "dependency2statfun.h"

dependency2 &d2()
{
static dependency2 dep2(d1());
return(dep2);
}

technique2b.cpp

// c10: technique2b.cpp
// (L) dependency1statfun dependency2statfun

#include "dependency2statfun.h"

int main()
{
d2();
}///:~
/* result:
dependency1 construction
dependency2 construction dependency1 init:1
*/

这个方法显然更加干净清爽:)
dependency1.h * 已知头文件
dependency2.h * 已知头文件
dependency1statfun.h * 设计头文件
dependency2statfun.h * 设计头文件2
dependency1statfun.cpp * 设计模块
dependency2statfun.cpp * 设计模块2
technique2b.cpp *main函数,用来测试
我们发现class dependency2的对象实例必须通过拷贝构造函数来初始化,而且他需要借助
class dependency1的对象。因此以下的初始化总是可能存在的:
dependency2 dep2((dependency1)dep1);
如果dep1和此定义不再同一模块中,我们显然要面对静态对象的依赖问题。

技巧2是这样作的。他通过定义两个模块提供了两个函数d1()和d2(),他们分别包含一个静
态对象dep1和dep2(d1),如果要创建dependency2对象,只有通过d2()返回得到。实际上,
技巧2正是封锁了初始化方法,迫使初始化按照即定的顺序进行。
dependency2& -> d2() -> dep2(d1()) -> d1() -> dep1 -> dependency1: dependency1()

总结

我们看到static带来功能性的同时,也带来了困惑,因为在某些情况下,它用来控制存储位
置,有些时候控制可视范围或者名字的链接属性。
* static存储在静态数据内存区,拥有全局生命域
* 作用在局部变量上,static使得变量局部可见却拥有全局生命期。
* 作用在全局变量上,static控制可视范围,仅仅在本模块内可见。
* 作用在类的数据成员上,static既控制生命期,又限制该标识符仅仅在类中可见。
* 作用在类的成员方法上,static方法除了保留数据成员特性以外,还去除了指针。这意味
着,静态成员不能访问非static数据成员,也不能调用非static成员方法。

namespace 同样给予更加灵活的方法,让您在大工程内部控制代码的增加和繁衍。

class 内部的class是在程序中控制名字的另外一种方法,他不会和全局名字冲突,而在程
序内部享用独立的可视范围和访问控制。这会大大增强你代码的维护性能。