c语言高级编程指南1 (翻译)

(译者注:

原文地址http://pfacka.binaryparadise.com/articles/guide-to-advanced-programming-in-C.html

本文是第一部分,翻译的内容为int类型转换,内存分配。)


c语言是系统编程,嵌入式系统以及很多其他应用程序的一种选择。然而似乎对计算机编程不是特别感兴趣,就不会去接触c语言。熟悉c语言的各个方面,了解这个语言的各种细节是一个巨大的挑战。本文试着提供深入的资料来阐述其中的一部分内容。包括:int类型的转换、内存分配、数组的指针转换、显式内联函数、Interpositioning(译者未找到合适的词语翻译) 、向量变化。

int 变量溢出和类型转换

很多c语言程序员都认为对int类型基本的操作是安全的,所以使用的时候不会做过多的审查。实际上这些操作很容易出问题。思考下后面的代码:
int main(int argc, char** argv) {
    long i = -1;


    if (i < sizeof(i)) {
         printf("OK\n");
    }
    else {
         printf("error\n");
    }


    return 0;
}

(译者注:结果是error,出乎很多人的意料吧,下面是作者的解释。)

导致这样的原因是变量i被转换成了unsigned int类型。所以它的值不再是-1,而是size_t的最大值,这是由sizeof操作符的类型导致的。


具体的原因在C99/C11标准的常用算术转换章节中找到:
“如果操作符中有unsinged int类型,并且这个操作符的优先级高于或者等于其他操作符的时候,需要将signed int转换为unsinged int。”

c语言标准中要求size_t长度至少16字节,unsinged int 类型。通常size_t的位数是和系统相关的,int类型的大小和size_t至少是相等的,于是上述的规则强行的把变量转换为unsinged int.

(译者注:关于sizof的介绍可以查看http://blog.csdn.net/sword_8367/article/details/4868283)

在我们使用int类型大小的是,就会存在一些问题。c语言标准并没有明确的定义short, int ,long ,long long 以及他们unsinged版本的大小。只是把最小的大小强制规定了。以x86_64框架为例子,long类型在linux上为64个字节,相反在64位的windows上仍然是32个字节。为了使代码更好的移植,通常的方法是用长度固定的类型,例如unit16_t 或者int32_t, 他们在C99标准的stdint.h头文件中定义。下面三种int类型在那里被定义:
1明确大小的:uint8_t uint16_t int32_t 等等
2定义类型的最小长度的:uint_least9_t, uint_least16_t, int_least32_t等
3最高效,定义最小长度的:uint_fast8_t, uint_fast16_t, int_fast32_t等

但是不幸的是,使用stdint.h并不能避免所有的问题。”integral promotion rule"(int类型转换的规则)里面这样说:
If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.
如果一个int可以表现原始类型所有的值,那么这个值被转换为int,否则转换为unsinged int .这叫做int类型转换,所有其他的类型在int类型转换中不会被改变。
下面的代码在32为上结果为65536, 在16为机器上为0;
uint32_t sum()
{
    uint16_t a = 65535;
    uint16_t b = 1;
    return a+b;
}

int类型转换保持变量的符号,不过一个简单的char类型转换是被转换为有符号数还是无符号数呢?

通常char类型转换要依靠硬件结构和操作系统,通常在特定平台的程序二进制接口中被确定的。如果你发现char被提升为siged char ,下面的代码会打印-128,127(例如x86框架),否则为128,129.gcc加上编译选项-funsigned-char强制的将x86平台上提升为无符号数。
char c = 128;
char d = 129;
printf("%d,%d\n",c,d);

内存分配和内存管理

malloc, calloc,realloc,free
malloc分配一个以bytes为单位的,指定大小的,未初始化的内存空间。如果大小为0,返回结果取决于操作系统,在c语言或者POSIX中没有明确说明这个行为.

如果空间大小必须是0,这个结果由编译器决定:返回一个空指针或者是唯一的指针。

malloc(0)通常会返回一个唯一的合法的指针。任何一种返回方式,必须保证在调用free函数的时候,不能报错。其中空指针,free函数不会做任何操作。

所以如果以一个表达式的结果作为malloc的参数的时候,需要测试int越界。

size_t computed_size;

if (elem_size && num > SIZE_MAX / elem_size) {
    errno = ENOMEM;
    err(1, "overflow");
}

computed_size = elem_size*num;


void * calloc(size_t nelem, size_t elsize);
一般情况下,分配一系列相同大小的空间的时候,应该使用calloc,这样不用表达式去计算大小()。另外它会初始化内存空间为0.释放分配的空间,使用free.

void* realloc(void* ptr, unsigned newsize);
realloc将会改变之前分配内存的大小。函数返回的指针指向新的内存位置,里面的内容可能会和原来的内容有相同的部分。如果新分配的大小比原来的大,增加的空间就可能没有被初始化。如果参数中旧指针为空,大小不等于0,那么作用等同于malloc。如果参数中大小为0,旧指针非空,那么产生的结果取决于操作系统。

大部分操作系统去释放旧指针的内存,返回malloc(0)或者返回NULL.例如,windows会释放内存,并且返回NULL,OpenBSD也会释放内存,并且会返回指向大小为0的指针。

如果realloc失败了会返回NULL,并且会留下曾经分配的内存。所以不仅要检测参数是否溢出,当realloc分配失败的时候,还要正确处理旧的内存空间。
#include <stdio.h>
#include <stdint.h>
#include <malloc.h>
#include <errno.h>


#define VECTOR_OK            0
#define VECTOR_NULL_ERROR    1
#define VECTOR_SIZE_ERROR    2
#define VECTOR_ALLOC_ERROR   3


struct vector {
    int *data;
    size_t size;
};


int create_vector(struct vector *vc, size_t num) {


    if (vc == NULL) {
        return VECTOR_NULL_ERROR;
    }


    vc->data = 0;
    vc->size = 0;


    /* check for integer and SIZE_MAX overflow */
    if (num == 0 || SIZE_MAX / num < sizeof(int)) {
        errno = ENOMEM;
        return VECTOR_SIZE_ERROR;
    }


    vc->data = calloc(num, sizeof(int));


    /* calloc faild */
    if (vc->data == NULL) {
        return VECTOR_ALLOC_ERROR;
    }


    vc->size = num * sizeof(int);
    return VECTOR_OK;
}


int grow_vector(struct vector *vc) {


    void *newptr = 0;
    size_t newsize;


    if (vc == NULL) {
        return VECTOR_NULL_ERROR;
    }




    /* check for integer and SIZE_MAX overflow */
    if (vc->size == 0 || SIZE_MAX / 2 < vc->size) {
        errno = ENOMEM;
        return VECTOR_SIZE_ERROR;
    }


    newsize = vc->size * 2;


    newptr = realloc(vc->data, newsize);


    /* realloc faild; vector stays intact size was not changed */
    if (newptr == NULL) {
        return VECTOR_ALLOC_ERROR;
    }


    /* upon success; update new address and size */
    vc->data = newptr;
    vc->size = newsize;
    return VECTOR_OK;
}


避免致命错误

在动态内存申请上,避免错误的通用方法,小心翼翼的写代码,尽可能的做好异常保护。但是有很多常见的问题上,有一些方法可以避免他们。

1 )重复调用free导致崩溃

这个问题由free函数的参数为下列情况引起:空指针,或者指针没有用malloc等函数分配的(野指针),或者已经被free/recalloc释放了(野指针)。为了避免这个问题,可以采取下列方法:
1 如果不能立即给指针赋有效的值,那么在声明的时候,初始化指针为NULL,
2 gcc 和clang 都会对未初始化的变量进行警告。
3 不要用同一个指针去指向静态内存和动态内存。
4 在使用free之后,将指针设置为NULL, 这样如果你不小心又调用free,也不会出错。
5 为了避免两次释放,在测试和调试的时候,使用assert 或许类似的函数。


char *ptr = NULL;
/* ... */

void nullfree(void **pptr) {
    void *ptr = *pptr;
    assert(ptr != NULL)
    free(ptr);
    *pptr = NULL;
}

2 )通过空指针或者未初始化的指针访问内存。

使用上述规则,你的代码只需要处理空指针或有效的指针。只需要在函数或者代码段开始的时候,检测动态内存的指针是否为空。

3 )访问越界的内存

 访问越界的内存并不一定都会导致程序崩溃。程序可能继续操作使用错误的数据,产生危险的后果,或者程序可能利用这些操作,进入其他的分支,或者进入执行代码。逐步的人工检测数组边界和动态内存边界,是主要避免这些危险的主要方法。内存边界的信息可以人工跟踪。数组的大小可以用sizeof函数,但是有时候array也会被转换为指针,(例如在函数中,sizeof 会返回指针的大小,而不是数组。)c11标准中的接口Annex k 是边界检测的接口,定义了一系列新库函数,提供了一些简单安全的方法去代替标准库(例如string 和I/O操作) 还有一些开源的方法例如 slibc,但是他的接口没有广泛采用。基于BSD系统(包括Mac OS X)提供了strlcpy,strlcat函数,可以更好的进行字符串操作。对于其他系统可以使用libbsd libraray.很多操作系统提供了接口,控制获取内存区域,保护内存读写例如posix mporst,这些机制主要用于整个内存页的控制。

避免内存泄露

内存泄露是由于有些动态内存不在使用了,但是程序没有释放而导致的。所以真正理解分配的内存空间作用域,最重要的是 free函数什么时候调用。但是随着程序复杂性的增强,这个就会变得越来越困难,所以在开始的设计中需要加入内存管理的功能。下面是一些方法去解决这些问题:

1)启动的时候申请

 将所有需要的堆内存分配防止程序启动的时候可以让内存管理变得简单。在程序结束的时候由操作系统释放(这里的意思是程序结束调用free么?还是程序关闭后系统自己free)。在很多情况下,这个方法是令人满意的,特别是程序批处理输入,然后完成。 

 2)可变长度的数组

 如果你需要一个可变大小的临时存储空间,生命周期只在一个函数中,那么可以考虑使用VLA(可变长度数组)。但是使用它是受限制的,每个函数使用它的空间不能超过百个字节。因为可变长度数组在C99中定义的(C11优化)有自动存储区域,它和其他的自动变量一样有一定的范围。尽管标准没有明确指出,通常会将VLA放在栈空间中。 VLA的最大可以分配的内存空间大小为 SIZE_MAX字节。先要知道目标平台的栈空间大小,我们要谨慎使用,确保不出现栈溢出,或者读取内存段下面的错误数据。 

 3)人工引用计数

 这个技术的背后思想是记录每次分配和失去引用的数目。在每次分配引用的时候计数增加,每次失去引用的时候分配减少。当引用的数目为0的时候,表示内存空间不再使用了,然后进行释放。但是C语言不支持自动析构(实际上,GCC和Clang都支持cleanup扩展)但并不意味着要重写分配操作符,通过人工的调用retain/release来完成计数。 函数。换一个思路,程序中有多个地方会占用或者解除和一块内存空间的关系。即便是使用这个方法,要遵守很多准则来确保不会忘记调用release(导致内存泄露)或过多的调用(提前释放)。但是如果一个内存空间的生命期,是由由外部事件确定,并且程序的结构决定,它会用各种方法来处理内存空间,那么使用这种麻烦的方法也是很值得的。下面代码块是一个简单的引用计数去进行内存管理。 
 #include <stdlib.h>
#include <stdint.h>


#define MAX_REF_OBJ 100
#define RC_ERROR -1


struct mem_obj_t{
    void *ptr;
    uint16_t count;
};


static struct mem_obj_t references[MAX_REF_OBJ];
static uint16_t reference_count = 0;


/* create memory object and return handle */
uint16_t create(size_t size){


    if (reference_count >= MAX_REF_OBJ)
        return RC_ERROR;


    if (size){
        void *ptr = calloc(1, size);


        if (ptr != NULL){
            references[reference_count].ptr = ptr;
            references[reference_count].count = 0;
            return reference_count++;
        }
    }


    return RC_ERROR;
}


/* get memory object and increment reference counter */
void* retain(uint16_t handle){


    if(handle < reference_count && handle >= 0){
        references[handle].count++;
        return references[handle].ptr;
    } else {
        return NULL;
    }
}


/* decrement reference counter */
void release(uint16_t handle){
    printf("release\n");


    if(handle < reference_count && handle >= 0){
        struct mem_obj_t *object = &references[handle];


        if (object->count <= 1){
            printf("released\n");
            free(object->ptr);
            reference_count--;
        } else {
            printf("decremented\n");
            object->count--;
        }
    }
}



如果你不考虑各个编译器的兼容性,你可以使用cleanup attribute在c语言中模仿自动析构。
(参考http://blog.csdn.net/haozhao_blog/article/details/14093155
http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html)
void cleanup_release(void** pmem) {
    int i;
    for(i = 0; i < reference_count; i++) {
        if(references[i].ptr == *pmem)
           release(i);
    }
}


void usage() {
    int16_t ref = create(64);


    void *mem = retain(ref);
    __attribute__((cleanup(cleanup_release), mem));


    /* ... */
}




cleanup_release的另一个缺点是根据对象的地址去释放,而不是根据引用的个数。因此cleanup_release 在引用数组的查找上耗费巨大。一种补救的方法是是修改retain的接口,返回指向结构体mem_obj_t的指针。另外一种方法是用下面的宏,它创建变量去保存引用的数目,并且和cleanup attribute相关联。




/
* helper macros */
#define __COMB(X,Y) X##Y
#define COMB(X,Y) __COMB(X,Y)
#define __CLEANUP_RELEASE __attribute__((cleanup(cleanup_release)))


#define retain_auto(REF) retain(REF); int16_t __CLEANUP_RELEASE COMB(__ref,__LINE__) = REF


void cleanup_release(int16_t* phd) {
    release(*phd);
}


void usage() {
    int16_t ref = create(64);


    void *mem = retain_auto(ref);
    /* ... */
}




4 内存池

如果一个程序运行的时候会经过很多步骤,在每一个步骤开始的时候可能有内存池。任何时候程序需要分配内存的时候,其中的一个内存池就会被使用。根据分配内存的生命周期去选择内存池,并且内存池属于程序的某个阶段。在每一个阶段结束,内存池被立刻释放。这个方法在长期运行的程序十分受欢迎,例如守护进程,它可以在整体上降低内存的碎片化。下面是内存池管理的一个简单例子。

#include <stdlib.h>
#include <stdint.h>

struct pool_t{
    void *ptr;
    size_t size;
    size_t used;
};

/* create memory pool*/
struct pool_t* create_pool(size_t size) {
    struct pool_t* pool = calloc(1, sizeof(struct pool_t));


    if(pool == NULL)
        return NULL;


    if (size) {
        void *mem = calloc(1, size);


        if (mem != NULL) {
            pool->ptr = mem;
            pool->size = size;
            pool->used = 0;
            return pool;
        }
    }
    return NULL;
}


/* allocate memory from memory pool */
void* pool_alloc(struct pool_t* pool, size_t size) {


    if(pool == NULL)
        return NULL;


    size_t avail_size = pool->size - pool->used;


    if (size && size <= avail_size){
        void *mem = pool->ptr + pool->used;
        pool->used += size;
        return mem;
    }


    return NULL;
}


/* release memory for whole pool */
void delete_pool(struct pool_t* pool) {
    if (pool != NULL) {
        free(pool->ptr);
        free(pool);
    }
}




实现一个内存池,是一个比较困难的事情。或许一些存在的库可以满足你的需求。
GNU libc obstack
Samba talloc
Ravenbrook Memory Pool System

5) 数据结构

很多内存管理的问题可以归结为使用正确的数据结构去存储数据。选择哪种数据结构主要是由访问数据,保存数据的算法需求来决定的,类似于使用链式表,哈希表,树等能带来额外的增益,例如遍历数据结构和快速释放数据。尽管在标准库中没有支持数据结构,但是下面有一些有用的库。


For traditional Unix implementation of linked lists and trees see BSD's queue.h and tree.h macros both are part of libbsd.
GNU libavl
Glib Data Types
For additional list see http://adtinfo.org/index.html


6 )标记和清理垃圾收集器

另一种方法是使用自动垃圾回收机制,从而减少人工的释放内存。指针引用是内存不使用的时候就想释放,而垃圾机制相反,是由特定事件触发的,例如内存分配失败,或者分配达到某个水平线。标记和扫除算法是实现垃圾机制的一种方法。一开始,它会遍历堆空间中所有的以前分配的内存引用,标记哪些还可以达到的引用,清理哪些没有被标记的引用。


或许,在c语言中最出名的垃圾收集机制为Boehm-Demers-Weiser conservative garbage collector。垃圾机制的缺点是性能开销和导致程序有不确定的停顿。另一个问题是有malloc引起的,它不能被垃圾回收机制管理,需要人工管理。
另外无法预知的停顿在实时系统中是不能接受的,但是很多环境上还是优点大于缺点。在性能的一方面,他们甚至宣称为高性能的。Mono project GNU Objective C runtime和Irssi IRC client都使用了Boehm GC。

内容简介回到顶部↑ 本书是专为在UNIX平台下用C语言编制程序的人写的。是以POSIX为标准,主要以C语言为基础,详细介绍了UNIX平台下编写各种应用程序的范例和方法。全书分四个部分,共十五章。本书范例丰富,且具有代表性,如Socket编程、客户/服务端编程、多线程开发、CGI编程、X Windows下的Motif编程等。读者直接或只需稍作修改就可以将它们应用到自己的应用程序开发中。这些范例的源代码可以从配套光盘的电子书中直接拷贝使用。 目录回到顶部↑ 第一部分 基本的系统调用 第1章 文件子系统 1.1 文件子系统的基本概念 1.2 基本的文件输入和输出 1.3 高级的文件操作 第2章 终端操作 2.1 终端的基本概念 2.2 终端输入和输出 2.3 ioctl系统调用 第3章 进程及进程间通信 3.1 进程的基本概念 3.2 进程的一般操作 3.3 进程的特殊操作 3.4 进程间使用管道通信 第4章 信号 4.1 信号的基本概念 4.2 信号机制 4.3 有关信号的系统调用 第5章 部分其他调用 5.1 系统调用 .5.2 相关函数 第二部分 网络编程 第6章 Socket编程基础 6.1 TCP/IP基础知识 6.2 Socket一般描述 6.3 Socket中的主要调用 6.4 Socket的原始方式 第7章 客户/服务器编程 7.1 客户端程序设计 7.2 服务器端程序设计 7.3 服务端程序结构 7.4 多协议(TCP、UDP)服务端 7.5 客户端的并发程序设计 7.6 使用telnet协议的客户端例子 第8章 线程 8.1 有关线程的基本概念 8.2 线程的创建和终止 8.3 线程控制调用 8.4 线程之间的互斥 8.5 线程之间的同步 8.6 线程特定数据区的函数调用 8.7 一个使用线程的客户端并发的例子 8.8 有关线程的函数列表 第9章 CGl编程 9.1 CGI程序的基本概念 9.2 CGI基本编程 9.3 使用脚本语言编写CGl 9.4 Perl语言简介 9.5 一个简单的CGI例子 第三部分 X Window应用程序开发 第10章 X Window和Motif基础 10.1 简介 10.2 XWindow基本概念 10.3 启动Motif窗口管理器 10.4 设置Motif特性 10.5 Widget 第11章 Motif编程 11.1 基本编程概念 11.2 Widget资源 11.3 Motif编程基础 11.4 程序框架 11.5 "HelloWorld!"示例 11.6 管理器 11.7 按钮 11.8 X事件 11.9 其他Widget简介 11.10 菜单 11.11 对话框 第12章 Widget与X事件汇总 12.1 Widget 12.2 X事件 第四部分 常用的编程工具 第13章 编译器及调试工具 13.1 编译器用法入门 13.2 调试器使用入门 13.3 关于库的简介 第14章 Make工具及makefile规则 14.1 概述 14.2 make和makefile的关系 14.3 makefile规则 14.4 伪指令 14.5 make命令行参数 第15章 版本控制 15.1 版本控制概念 15.2 源代码控制系统SCCS 15.3 RCS使用方法 15.4 并发版本控制CVS
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值