[C和指针]第二部分

 

第四章     指针... 1
第五章     函数... 14
第六章     数组... 17
第七章     字符()/... 25

第四章     指针


指针代表地址,即使用名字来替换地址,它存储的就是一个地址值。名字与内存位置之间的关联并不是硬件所提供的,它是由编译器为我们实现的。这些指针变量名给了我们一种更方便的方法记住地址——硬件仍然通过地址访问内存的位置。
 
每个变量都是包含了一序列内容为01的位,它们可以被解释为整数,也可以被解释为浮点数,这取决于被使用的方式。如果使用的是整型算术指令,这个值就被解释为整数,如果使用的是浮点型指令,它就是个浮点数。
 
 
指针图示:
clip_image002
注意箭头起始于方框内部,因为它代表存储于该变量的值。同样,箭头指向一个位置,而不是存储于该位置的值,这种记法提示跟随箭头执行间接访问操作的结果将是一个左值(即某个变量的地址,如&a,而不是某个变量a的值)。
 
 
    int *a;
    *a =12;
上面的程序是极为危险与错误的。这个声明创建了一个名叫a的指针变量,后面那个赋值语句把12存储在a所指向的内存位置。因为从未对a进行初始化,所以我们没有办法预测12这个值将存储于什么地方。虽然如果变量a是静态的它会被初始化为0,变量为自动的,它根本不会被初始化,但无论是哪种情况,声明一个指向整型的指针都不会“创建”用于存储整型值的内存空间(只有声明一个非指针变量时才会分配存储空间)。该程序在不同的编译器中会产生不同的结果,运气好的话,提示内存错误,运行不好的话,程序表面上会正常结束,而且*a值是正确的,但是这里隐藏了一个极为严重的错误并且一旦出现很难发现:a会覆盖内存中原先随机初始化指向的某个位置的值。所以,在对指针进行间接访问之前,必须非常小心,确保它们已被初始化,如:
    int b, *a;//此时的指针随机指向某个存储空间
    printf("%d\n", a);//随机指向的位置:2147348480
    a = &b;//初始化指针,让它指向某个确定的内存空间
    printf("%d\n", a);//现在指针所指向的位置为:2293576
    *a = 12;//12常量存储到a所指向的某个内存位置
    printf("%d\n", *a);//12
    printf("%d", b);//12
 
NULL指针不指向任何东西。要使指针变量为NULL,你可以给它赋一个零。为了测试一个指针变量是否为NULL,你可将它与零值进行比较。之所以选择零这个值是因为一种约定,就不同机器内部而言,NULL指针实际值可能不是零,在这种情况下,只要使用NULL,编译器将负责零值和内部值之间的翻译转换工作。
 
如果对一个NULL指针进行间接访问会发生什么情况呢?结果会因编译器而异,在有些机器上,它会访问位置为零的地址中的内容,但这是极其错误的;在其他机器上,对NULL指针进行间接访问将引发一个错误,并终止程序,这种情况下程序员就很容易提示发现错误,如XP就是这样的:
    //将字符数组(字符串就是数组)首地址赋给p1
    char *p1="a" ;
    printf("%c",*p1);//a
    //将指针指向内存地址为0的位置
    char *p2=NULL ;
    printf("%c",*p2);//!!出错 
 
  clip_image002[4]
    int a;
    *&a=12;
上面程序的结果就是将12赋值给变量a&操作符产生变量a的地址,它是一个指针常量,接着,*操作符访问其操作数所表示的地址,在这个表达式中,操作数是a的地址,所以25就存储到a中。这与  a = 12 有区别吗?从结果上讲没有区别,但它涉及到更多的操作。
 
假定a存储于位置100,下面这条语句合法吗?
*100 = 12;
这是错误的,因为字面值100的类型是整型值,而间接访问操作只能作用于指针类型表达式。如果你确实想把12存储于位置100,你必须使用强制类型转换:
*(int *)100 = 12;//100从整型转换为指向整型的指针类型
虽然强转后合法,但通常我们不会这样作,因为我们无法预测编译器会把某个特定的变量放在内存中的什么位置,所以你无法预先知道变量a的地址。这个技巧除非用来访问内存中某个特定的位置,不是某个变量,而是访问硬件本身。
 
 
二级指针(指针的指针):
    int a = 12;
    int *b = &a;
    int**c = &b; 
clip_image002[6]
 

表达式

等效的结果

a

12

b

&a

*b

a , 12

c

&b

*c

b , &a

**c

*b , a, 12

 
 
 
 
 
 
各种指针表达式示例
    char ch = 'a';
    char *cp = &ch;
现在我们有了两个变量,它们初始化如下:

 

clip_image002[8]

表达式

右值

左值

描述

ch

clip_image004

clip_image006

作为右值使用时,粗椭圆表示变量ch的值就是表达式的值(右值为读取);但是,当这个表达式作为左值使用时,它是这个内存的地址而不是该地址所包含的值,此时该位置用粗方框标记(左值为写入,可存储值),表示这个位置就是表达式的结果,另外,它的值并未显示,因为它并不重要,事实上,这个值将被某个新的值所取代。

&ch

clip_image008

非法

作为右值,这个表达式的值是变量ch的地址,这个值同变量cp中所存储的值一样,但这个表达式并没有涉及到cp变量,所以这个结果值并不是因为它而产生的,所以图中椭圆并没有画在cp的箭头周围;作为左值时,当表达式&ch进行求值时,它的结果肯定是存储在计算机的某个地方了,但我们无法知道,所以这个表达式的结果未标识机器内存中的特定位置,所以不是合法左值。

cp

clip_image010

clip_image012

右值就是cp的值,左值就是cp所处的内存位置。

&cp

clip_image014

非法

该表达式为指向指针的指针。这个值存储位置并未清晰定义,所以不是一个合法左值

 

 

 

*cp

clip_image016

clip_image018

现在加入了间接访问符*,所以现在使用实线

*cp+1

clip_image020

非法

图中虚线表示表达式求值时数据的移动过程。这个表达式的最终结果存储位置并未清晰定义,所以不是一个合法左值。

*(cp+1)

clip_image022

clip_image024

指针加法运算的结果是一个右值,因为它存储位置并未清晰定义。如果这里没有间接访问操作*,这个表达式将不是一个合法的左值。然而,间接访问跟随指针访问一个特定的位置,这样*(cp+1)就可以作为左值使用,尽管 cp+1 本身并不是左值。间接操作符是少数几个其结果为左值的操作符之一。

++cp

clip_image026

非法

cp本身先加1,再拷贝,椭圆即为拷贝出来的内容。

cp++

clip_image028

非法

先拷贝cp,再将cp本身加1,椭圆即为拷贝出来的内容。

*++cp

clip_image030

clip_image032

 

*cp++

clip_image034

clip_image036

 

++*cp

clip_image038

非法

 

(*cp)++

clip_image040

非法

 

++*++cp

clip_image042

非法

虚线椭圆为++cp的中间结果,虚线方框为*++cp的中间结果,实线椭圆为 ++*++cp 最终结果

++*cp++

clip_image044

非法

 

 


求字符串长度(指针数组的副作用): 
clip_image002[10]
#include<stdio.h>
//指针数组名就是一个二级(多级)指针
int find_char(char ** strings, int value) {
    //循环列表中的每个字符串
    while (*strings != NULL) {
       //判断字符串中的每个字符,查是否已经找到
       while (**strings != '\0') {
           //strings为第一级指针,即数组名,可以指向每个元素。*strings为第二级指针,指向了元素中的某个具体位置。(*strings)++二级指针可以在元素中移动,这里就修改了二级指针,并将修改后的值保存在了指针数组中,所以不能重新调用。
           char c = *(*strings)++;
           if (c == value) {
              return 1;
           }
       }
       //strings为第一级指针,strings++在元素之间移动,这里修改了一级指针,但主函数调用时是传值,传递的是第一级指针的值,并未修改主函数中指针数组首元素地址
       strings++;
    }
    return 0;
}
int main(int argc, char **argv) {
    /*
     * 注,不能这样定义 char **c = { "a", "b", "c", NULL };
     * 原因就像:int *a; *a =12;一样,定义指针变量时并没有为
     * 他创建用于存储相应内容的空间,这些指针的值是随机的,所
     * 以上面这样直接赋值是不可预测的,一般会出现内存读写错误
     */
    char *c[] = { "abcd", "efgh", "ijkl", "mnop", NULL };
    //指针数组名就是一个二级(多级)指针
    char **b = c;//只能先经过上面定义后,再将数组名赋给二级指针
    printf("%d", find_char(c, 'j'));
    printf("%d", find_char(b, 'j'));//不能进行第二次查询?
}
第一次调用find_char函数,内存情况如下: 
clip_image002[12]
第一次刚进入find_char函数,内存情况如下: 
clip_image002[14]
find_char函数第一次调用,内存情况如下: 
clip_image002[16]
第二次刚进入find_char函数,内存情况如下: 
clip_image002[18]
从上面内存调度来看,第一级指针未修改,但第二级指针已被修改,所以修改了主调函数中的源数组。如果不想有这样的副作用,则可以这样:
int find_char(char ** strings, int value) {
    char *string;//当前正在查找的字符串,使用一个临时变量为存储第二维各元素(字符串)指针,这样在各字符串中通过该临时指针移动时,不会改变原数组本身的各元素(字符串)指针位置。
    //循环列表中的每个字符串
    while ((string = *strings++) != NULL) {
       //判断字符串中的每个字符,查是否已经找到
       while (*string != '\0') {
           char c =*string++;
           if (c == value) {
              return 1;
           }
       }
    }
    return 0;
}
 
常量数组与地址
常量数组不代表地址,这与字符串常量是不一样的,如下:
    int a1[] = { 1, 2, 3, 0 };
    int a2[] = { 4, 5, 6, 0 };
    int a3[] = { 7, 8, 0 };
    /*
     * 注,不能这样初始化  int *c[] = { { 1, 2, 3, 0 }, { 4, 5, 6, 0 }
     * , { 7, 8, 0 }, NULL }; 原因就是{ 1, 2, 3, 0 }并不能表示数组本
     * 身地址,这与字符串常是有区别的
     */
    int*c[] = { a1, a2, a3, NULL };
 
二级指针与二维数组
二级指针与不等同于二维数组名,数组(无论是多少维的)名永远是第一个元素的地址,如果是多维,则所有维度的首元素地址都相等(所有维度都是对齐的),测试如下:
    //一维数组
    char a[9] = { 0 };
    sprintf(a,"%p",&a[0]);
    //二维数组
    char aa[][9] = { { 0 } };
    sprintf(aa[0],"%p",&aa[0][0]);
    //三维
    char aaa[][1][9] = { { { 0 } } };
    sprintf(aaa[0][0],"%p",&aaa[0][0][0]); 
clip_image002[20]
指向数组的指针(数组指针)都是一级指针,而不是多级指针(即数组的维度数并不等于指针的级别数):
    char a[2] = { 0 };
    char *p1 = a; //指向一维数组的指针
    *(p1 + 1) = 'a';//修改第二个元素的值
    char a1 = *(p1 + 1);
    char aa[][2] = { { 0 } };
    char (*p2)[2] = aa; //指向二维数组的指针
    /*
     * 不能写成*(p2 + 1),否则报不能将int类型转换成char[2]类型
     */
    *(*p2 + 1) = 'b'; //修改第二个元素的值
    char a2 = *(*p2 + 1);
    char aaa[][1][2] = { { { 0 } } };
    char (*p3)[1][2] = aaa; //指向三维数组的指针
    /*
     * 不能写成*(p3 + 1),否则报不能将int类型转换成char[1][2]类型
     */
    *(**p3 + 1) = 'c'; //修改第一个元素的值
    char a3 = *(**p3 + 1); 
 clip_image002[22]
 
#include<stdio.h>
int main() {
    //多维数组只有第一维可以省略,第三个元素没有指定内容,这里会自动初始化为0
    int a[][3] = { { 11, 12,14 }, { 21, 22, 23 } };
    int (*p)[3] = a;//数组指针
    //可以有以下四种方式访问a[1][0]元素,方括号[]*
    //先级高,所以下面是可以省略的
    printf("1%d %d\n", a[1][0],p[1][0]);//21
    /*
     * (a+1)指向二维数组中的第二个元素数组;(a + 1)[0]在第二个元素数组中移动,
     * 即指向第二个元素数组中的首元素,结果是一个地址;*((a + 1)[0])取第二
     * 个元素数组中的首元素
     */
    printf("2%d %d\n", *((a + 1)[0]),*((p + 1)[0]));//21
    /*
     * (a+1)指向二维数组中的第二个元素数组;(*(a + 1))取第二个元素数组,结果是
     * 一个一维数组;(*(a + 1))[0]取第二个元素数组中的首元素
     */
    printf("3%d %d\n", (*(a + 1))[0],(*(p + 1))[0]);//21
    /*
     * a[1]指向第二个元素数组, *(a[1])取第二个元素数组中的首元素
     */
    printf("4%d %d\n", *(a[1]),*(p[1]));//21
    /*
     * (a+1)指向二维数组中的第二个元素数组,*(a+1)取第二个元素数组,
     * **(a + 1)取第二个元素数组中的首元素
     */
    printf("5%d %d\n", **(a + 1),**(p + 1));//21
 
    //在定义时可以不指定指针移动的单位,即指针单位长度
    //但如果不指定,则不能对指针进行移动操作,否则编译出错
    int (*p1)[] = a;
    //在不移动指针的条件下可以访问第一个元素
    printf("6%d\n", **p1);//11
    //编译不能通过,因为上面在定义p1时未指定指针的最小移动单位
    //所以不能移动
    //  printf("%d", **(p1 + 1));
 
    //一维数组指针
    int b[]={1,2,3};
    int *p2=b;
    printf("7%d\n", *(p2 + 1));//2
 
    //指定长度后就可以对指针进行移动
    int (*p3)[3] = a;
    /*
     * 3int为单位进行移动。(p3 + 1)在第一维中移动,
     * (*(p3 + 1)+1)在第二维中移动
     */
    printf("8%d\n", *(*(p3 + 1)+1));//22
 
    //编译时警告。以2int单位进行移动
    int (*p4)[2] = a;
    printf("9%d\n", **(p4 + 1));//0
} 
 clip_image002[24]
 
高效数组循环方法
float values[5];
float *vp;
for (vp = &values[0]; vp < &values[5];) {
    *vp++ = 0;
}
for语句使用了一个关系测试来决定是否结束循环,这是合法的,因为vp和指针常量都指向同一数组中的元素(事实上,这个指针常量所指向的是数组最后一个元素后面的那个内存位置,虽然在最后一次比较时,vp也指向了这个位置,但由于我们此时未对vp执行间接访问操作,所以它是安全的)
上面的循环等同于下面的循环:
for (vp = &values[5]; vp > &values[0];) {
    *--vp = 0;
}
但不能使用以下循环:
for (vp = &values[4]; vp >= &values[0];) {
    *vp-- = 0;
}
原因就是:比较表达式vp >= &values[0];的值未定义,因为vp移到了数组的边界之外。标准允许指向数组的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但不允许与指向数组第一个元素之前的那个内存位置的指针进行比较。所以最后不要这样使用,但在大多数的编译器中能顺利的完成任务,但为了可移植性与稳定性,不能这样使用。
 
 
 
 
 
声明一个指针变量并不会自动分配任何内存。在对指针执行间接访问前,指针必须进行初始化:或者使它指向现有的内存,或者给它分配动态的内存。对未初始化的指针变量执行间接访问操作是非法的,而且这种错误常常难以检测。其结果常常是一个不相关的值被修改。这种错误是很难被调度发现的。
 
NULL指针就是不指向任何东西的指针。它可以赋给一个指针,用于表示那个指针并不指向任何值。对NULL指针执行间接访问操作的后果因编译器而异,两个常见的后果分别是返回内存位置零的值以及终止程序
 
对指针执行间接访问操作所产生的值也是个左值,因为这种表达式标识了一个特定的内存位置。

第五章     函数


K&R C:
int *
find_int(key,array,array_len)
int key;
int array;
int array_len;
{}
但不能省略函数的大括号,所以不支持函数原型声明。
 
为了与ANSI标准之前的程序兼容性,没有参数的函数的原型应该写成下面这样:
int func(void);
 
当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型值。所有函数都应该具有原型,尤其是那些返回值不是整形的函数
 
可变参数
可以使用<stdarg.h>中的一组宏定义来操纵可参数。
 
用法:     
1)首先在函数里定义一具va_list型的变量,这个变量是指向参数的指针
2)然后用va_start宏初始化变量刚定义的va_list变量,这个宏的第二个参数是省略号前最后一个有名字的参数。
3)然后用va_arg返回可变的参数,va_arg的第二个参数是你要返回的参数的类型。如果函数有多个可变参数的,依次调用va_arg获取各个参数。
4)最后用va_end宏结束可变参数的获取。
 
由于参数列表中的可变参数部分并没有原型,所以,所有作为可变参数传递给函数的值都将执行缺省参数类型提升(charshortfloat会默认提升为intfloat会默认提升为double)。
 
参数列表中至少要有一个命名参数,如果边一个命名参数也没有,你就无法使用va_start。这个参数提供了一种方法,用于查找参数列表的可变部分。
 
#include<stdio.h>
#include<stdarg.h>
/*求和*/
int sum(int first, ...) {
    int sum = 0, i = first;
    va_list marker;//定义一具VA_LIST型的变量,这个变量是指向参数的指针
    /*first一般表示可变参数列表中的前面最近第一个有名参数,如果前面有多个有
     * 名参数,即使这里指定为前面最近第二个有名参数,但可变参数列表还是从最
     * 后一个有名参数后面算起,对无名可变参数列表没有影响,但编译时会出警告,
     * 另外,参数列表中至少要有一个命名参数,如果连一个命名参数也没有,就无法使用可变参数*/
    va_start( marker, first ); /* VA_START宏初始化变量刚定义的VA_LIST变量,
                            这个宏的第二个参数是第一个可变参数的前一个参数,
                            是一个固定的参数。*/
 
    while (i != -1) {
       sum += i;
       i = va_arg( marker, int);//VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型。
    }
    va_end( marker ); /* VA_END宏结束可变参数的获取     */
    return sum;
}
 
main(void) {
    /* Call with 3 integers (-1 is used as terminator). */
    printf("sum is: %d\n", sum(2, 3, 4, -1));//9
    /* Call with 4 integers. */
    printf("sum is: %d\n", sum(5, 7, 9, 11, -1));//32
    /* Call with just -1 terminator. */
    printf("sum is: %d\n", sum(-1));//0
}
 
 
C语言中的数据封装
 
/*
** 地址列表模块的声明。
*/
 
/*
** 数据特征
**
** 各种数据的最大长度(包括结尾的NULL字节)和地址最大数量。
**
*/
#define    NAME_LENGTH   30     /* 允许出现的最长名字
#define    ADDR_LENGTH   100    /* 允许出现的最长地址
#define    PHONE_LENGTH  11     /* 允许出现的最长电话号码
#define    MAX_ADDRESSES 1000       /* 允许出现的最多地址个数
 
/*
** 接口函数
**
** 给出一个名字,查找对应的地址
*/
charconst *
lookup_address(charconst *name);
 
/*
** 给出一个名字,查找对应的电话号码
*/
charconst *
lookup_phone(charconst *name);
                        ——addrlist.h
 
/*
** 用于维护一个地址列表的抽象数据类型
*/
 
#include"addrlist.h"
#include<stdio.h>
 
/*
** 每个地址的三个部分,分别保存于三个数组的对应元素中
*/
staticchar name[MAX_ADDRESSES][NAME_LENGTH];
staticchar address[MAX_ADDRESSES][ADDR_LENGTH];
staticchar phone[MAX_ADDRESSES][PHONE_LENGTH];
 
/*
** 这个函数在数组中查找一个名字并返回查找到的位置的下标
** 如果这个名字在数组中并不存在,函数返回-1
*/
staticint find_entry(charconst *name_to_find) {
    int entry;
 
    for (entry = 0; entry < MAX_ADDRESSES; entry += 1)
       if (strcmp(name_to_find, name[entry]) == 0)
           return entry;
 
    return -1;
}
 
/*
** 给定一个名字,查找并返回对应的地址
** 如果名字没有找到,函数返回一个NULL指针
*/
charconst *
lookup_address(charconst *name) {
    int entry;
 
    entry = find_entry(name);
    if (entry == -1)
       return NULL;
    else
       return address[entry];
}
 
/*
** 给定一个名字,查找并返回对应的电话号码
** 如果名字没有找到,函数返回一个NULL指针
*/
charconst *
lookup_phone(charconst *name) {
    int entry;
 
    entry = find_entry(name);
    if (entry == -1)
       return NULL;
    else
       return phone[entry];
}
                        ——addrlist.c

第六章     数组


字符数组与字符指针的区别:虽然字符指针指向的字符串内容是存放在某个数组中的,但是这与定义的字符数组是不样的,即字符指针所指向的数组的内容是不能被修改的;另外数组是一个常量指针,一旦初始化后,数组名就能不再指向其他数组了。
 
数组名并不是表示整个数组,它是一个指针常量,也就是数组第一个元素的地址,它的类型取决于数组元素的类型:如果它们是int类型,那么数组名的类型就是“指向int的常量指针”
 
数组具有确定的数量的元素,而指针只有是一个标量值,只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量,注意,这个值是指针常量,而不是指针变量,你不能修改常量的值,即数组一旦分配内存后,位置就定下来了,不会再被移动
 
函数如果要返回一个数组,则数组只能以指针的形式返回,而不能以数组的形式返回:
int * fun() {
    int a[]={1,2};
    return a;
}
int main(int argc, char **argv) {
    printf("%d", fun()[1]);//2
}
 
 
只有在两种场合下,数组名并不用常量指针来表示——就是当数组名作为sizeof操作符或单目操作符&的操作数时,所以下面 a&a是相等的,但普通的指针变量不是这样:
int a[] = { 1, 2, 3, 4, 5 };
    printf("%p\n", a);//0022FF30
    printf("%p\n", &a);//0022FF30
    printf("%d\n", sizeof a);//20
 
    intconst * const p = &b;
    printf("%p\n", p);//0022FF2C
    printf("%p\n", &p);//0022FF28
 
不能将一个数组名赋给另一个数组名,因为数组名是一个常量指针,如果需要拷贝数组,只能使用循环来一个个元素进行拷贝。
 
除了优先级之外,下标引用和间接访问完全相同,下面两个表达式是等同的:
    array[subscript]
    *(array + (subscript))
 
 
int main() {
    int array[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int *ap = array + 2;
    //ap{0022FF1C} == (array + 2){0022FF1C} == &array[2]{0022FF1C}
    printf("ap{%p} == (array + 2){%p} == &array[2]{%p}\n", ap, array + 2,
           &array[2]);
    //*ap{2} == *(array + 2){2} == array[2]{2}
    printf("*ap{%d} == *(array + 2){%d} == array[2]{%d}\n", *ap, *(array + 2),
           array[2]);
    //ap[0]{2} == *(ap + (0)){2} == array[2]{2}
    printf("ap[0]{%d} == *(ap + (0)){%d} == array[2]{%d}\n", ap[0],
           *(ap + (0)), array[2]);
    //ap+6{0022FF34} == (array + 8){0022FF34} == &array[8]{0022FF34}
    printf("ap+6{%p} == (array + 8){%p} == &array[8]{%p}\n", ap + 6, array + 8,
           &array[8]);
    //*ap+6{8} == array[2]+6{8}
    printf("*ap+6{%d} == array[2]+6{%d}\n", *ap + 6, array[2] + 6);
    //*(ap+6){8} == ap[6]{8}== array[8]{8}
    printf("*(ap+6){%d} == ap[6]{%d}== array[8]{%d}\n", *(ap + 6), ap[6],
           array[8]);
    //ap[-1]{1}==array[1]{1}
    printf("ap[-1]{%d}==array[1]{%d}",ap[-1],array[1]);
}
 
C语言中数组下标在编译时会转换为等效的指针运算:
int main() {

    int array[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    printf("%d\n", array[2]);//2
    printf("%d\n", 2[array]);//2
    printf("%d\n", *(2 + (array)));//2
    printf("%d\n", *(array + 2));//2
}
所以2[array]array[2]是等效的,这个诡异的技巧之所有可行,缘于C实现下标的方法,对编译器来说,2[array]== *(2 + (array))== *(array + 2)==array[2]
 
 
数组下标与指针效率的比较:
    int array[10], i;
    for (i = 0; i < 10; i++) {
       array[i] = 0;
    }
为了对下标表达式求值,编译器在程序中插入指令,取得i的值,并把它与整型的长度(也就是4)相乘。这个乘法需要花费一定的时间和空间。现使用指针来实现同样的功能:
    int array[10], *p;
    for (p = array; p < array + 10; p++) {
       *p = 0;
    }
现在这个乘法出现在for语句的调整部分,1这个值必须与整型的长度相乘,然后现与指针相加,但这里存在一个重大区别:循环每次执行时,执行乘法运算的都是两个相同的数(14),结果,这个乘法只在编译时执行一次,程序在运行时并不执行乘法运算,所以在运行时所需要的指令就少一些。
 
 
最高效的数组访问:
#define SIZE 50
int x[SIZE];
int y[SIZE];
//数组拷贝
int main() {
    registerint *p1, *p2;
    for (p1 = x, p2 = y; p1 < &x[SIZE];) {
       *p1++ = *p2++;
    }
}
结论:当你根据某个固定数目的增量在一个数组中移动时,不如使用指针变量将比使用下标产生效率更高的代码;声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高;如果你可以通过测试一些已经初始化并经过调整的内容来判断循环是否该应该终止,那么你就不需要使用一个单独的计数器。
 
指针和数组
    int a[5];
    int *b; 
clip_image002[26]
声明一个数组时,编译器将根据声明所指定元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置。声明一个指针变量时,编译器只为这个指针本身保留内存空间,它并不为任何整型值分配内存空间,而且,指针变量并未初始化为指向现有的有意义的空间。所以 *a 是完全合法的,但表达式 *b 却是非法的。*b将访问内存中某个不确定的位置,或者导致程序终止,另外,表达式b++可以通过编译,但a++却不行,因为a的值是个常量。
 
 
数组参数
数组名的值就是一个指向数组第一个元素的指针,所以传递给函数的是一份该指针的拷贝。函数如果执行了下标引用,实际上是对这个指针执行间接访问操作,并且通过这种间接访问,函数可以访问和修改主调程序的源数组元素。
 
传址调用是通过传递一个指向所需元素的指针,然后在函数中对该指针执行间接访问操作实现对数据的访问,而作为参数的数组名是个指针,下标引用实际执行的就是间接访问,所以传递数组时“好像”体现了传址,但本身却是传值,那数组的传值行为表现在什么地方呢?传递给函数的是参数的一份拷贝(指向数组起始位置的指针的拷贝),所以函数可以自由地操作它的指针形参,而不必提心会修改对应的作为实参的指针。所以并不矛盾:所有的参数都是通过传值方式传递的
 
传递一个数组时,正确的函数形参应该是怎样的?它是应该声明为一个指针还是一个数组?调用函数时实际传递的是一个指针。但为了使用程序员新手更容易上手一些,编译器也接受数组形式的函数形参,下面两个函数原型是相等的:
int strlen(char * string);
int strlen(char string[]);
这个相等性暗示指针和数组名实际上是相等的,但千万不要被它糊弄了,这两个声明确实相等,但只是在当前这个上下文环境中。哪个“更加准确”呢?答案是指针,因为实际上是个指针,而不是数组,同样,表达式sizeof string的值是指向字符的指针的长度,而不是数组的长度:
int size1(int * a){
    returnsizeof a;
}
int size2(int  a[]){
    returnsizeof a;
}
int main() {
    int a[5];
    printf("%d\n",sizeof a);//20
    printf("%d\n",size1(a));//4
    printf("%d\n",size2(a));//4
}
现在你应该清楚为什么函数原型中的一维数组形参无需写明它的元素数目(如果是多维数组,则也只能省略第一维的大小),因为函数并不为数组参数分配内存空间。形参只是一个指针,它指向的是已经在其他地方分配好的内存的空间,它可以与任何长度的数组匹配。另一方面,这种实现方法使函数无法知道数组的长度,如果函数需要知道数组的长度,它必须作为一个显示的参数传递给函数。
 
不完整的初始化
    int vector[5] = { 1, 2, 3, 4, 5, 6 };
    int vector[5] = { 1, 2, 3, 4 };
第一个声明是错误的,我们没有办法把6个整型值装到5个整型变量中去,但第二个声明是合法的,最后一个元素初始化为0。另外,也只能省略最后元素的值,不能省略中间的。
 
自动计算数组长度
    int vector[] = { 1, 2, 3, 4 };
如果声明中并未给出数组的长度,编译器就把数组的长度设置为刚好容纳所有的初始值的长度。
 
字符数组的初始化
    char string1[] = { 'H', 'e', 'l', 'l', 'o', 0 };
    char string2[] = "Hello";
第二种看上去是一个字符串常量,实际上并不是(只是一个初始化列表),它只是第一个声明的另一种写法。那什么情况下"Hello"是一个字符串常量呢?要根据它所处的上下文来区分:当用于初始化一个字符数组时,它就是一个初始化列表,在其他任何地坟,它都表示一个字符串常量。
 
    char string1[] = "Hello";
    char *string2 = "Hello";
这两个初始化看上去很像,但它们具有不同的含义。第一个初始化一个字符数组的元素,而后者则是一个真正的字符串常量,这个指针变量被初始化为指向这个字符串常量的存储位置,并且这个位置的内容不能被修改 
 
  clip_image002[28]
多维数组
多维数组在内存分配上与一维数组一样,在物理上没有区别,也是连续的,只是多维数组在逻辑上将元素划分为多个逻辑单位。
 
    int matrix[6][10];
    int *mp;
    matrix[3][8] = 38;
    matrix[3][9] = 39;
    matrix[4][0] = 40;
    mp = &matrix[3][8];
    printf("First value is %d \n", *mp);//38
    printf("Second value is %d \n", *++mp);//39
    printf("Third value is %d \n", *++mp);//40
这个例子使用一个指向整型的指针遍历存储了一个二维整型数组元素的内存空间,这个技巧被称为压扁数组,它实际上是非法的,因此从某行移到下一行后就无法回到包含第1行的那个子数组,尽管通常没有问题,但有可能的话还是应该避免这样使用。
 
多维数组名:
    int matrix[3][10];
matrix可以看作是一个一维数组,包含3个元素,只是每个元素又是包含了10整型元素的数组。matrix这个名字是一个指向它第1个元素的指针,所以matrix是一个指向一个包含10个整型元素的数组的指针
 
matrix[1][5] == *(*(matrix + 1) + 5) == *(matrix[1] + 5)
matrix 指向第一行,(matrix+1)指向第二行,*(matrix+1)代表第二行元素(第二维数组),既然*(matrix+1)又是一个数组,所以该表达式又代表了(matrix+1)指向的第二行子数组的首元素的地址,所以(*(matrix+1)+5)表示在(matrix+1)指向的第二行子数组中移动到第6个元素位置上,所以*(*(matrix+1)+5)代表了该位置上的元素。
 
int matrix[3,10];
在其他语言中表示二维数组,但在C中不是的,它是一个逗号表达式,最终的结果为matrix[10],而不是二维数组了
 
    int vector [10], *vp = vector;
    int matrix[3][10], *mp = matrix;
第一个表达式合法,vpvector具有相同的类型,都是指向整型的指针;第二个声明非法,因为matrix并不是一个指向整型的指针,而是一个指向整型数组的指针,正确写法如下:
         int (*p)[10]= matrix;
(*p)[10]由于间接访问*的优先级低于数组下标访问,所以使用括号,这样就使 *p[10]从数组类型转换成了一个指针类型,p是一个指向拥有10个整型元素的数组的指针,使用它可以在matrix数组中一行一行的移动。上述表达式指向了matrix数组的第一行。
如果你需要一个指针逐个访问整型元素而不是逐行在数组中移动,你应该这样:
    int *pi=&matrix[0][0]; //第一个元素的地址
    int *pi=matrix[0];//第一个元素的地址也就是数组名
上面两个声明都创建了一个简单的整型指针,并以两种不同的方式进行初始化,指向matrix的第1个整型元素。
 
如果你打算在指针上执行任何指针运算,应该避免这种类型的声明:
    int (*p)[] = matrix;
p仍然是一个指向整型数组的指针,但 数组的长度没有,当与某个整型运算时,它的值将根据空数组来进行调整,即与零乘,这不是我们想要的。有些编译器要以捕捉到这类错误,但有些编译器却不能。
 
int matrix[3][10];
如果传递给一个函数,则函数的原型声明可以有以下两种:
void func(int(*p)[10]);
void func(int p[][10]);
但决不能是:
void func(int **p);
因为**p的类型为指向指针的指针,而(*p)[10]表示指向一个具有10元素的数组的指针。
 
多维数组的初始化可以像一维数组那样:
    int matrix[2][3] = { 1, 2, 3, 4, 5, 6 };
Java不允许这样
 
    charconst* keyword[] = { "do", "for", "if", "register", "return",
           "switch", "while" };
    charconst**kwp = keyword;
keyword是一个指针数组,数组名keyword是一个二级指针(每个元素都是指针),其内存结构如下: 
 
clip_image002[30]
charconstkeyword[][9] = { "do", "for", "if", "register", "return",
           "switch", "while" };
其内存结构如下: 
clip_image002[34]

第七章     字符()/


C语言并没有的字符串数据类型,因为字符串是以字符串常量的形式出现或者存储于字符数组中。所有字符串都必须存储于字符数组或动态分配的内存中
 
字符串定义方式:
    char str1[10] = "abc";
    char *str2 = "abc";
 
NUL字节是字符串的终止符,但它本身并不是字符串的一部分,所以字符串的长度并不包括NUL字节。
 
库函数strlen的原型如下:
size_t strlen(charconst *string);
注意:strlen返回一个类型为size_t的值,这个类型是在头文件stddef.h中定义的,它是一个无符号整数类型。如果表达式中使用无符号数可能导致不可预料的结果。例如下面两个表达式看上去相同:
if(strlen(x) >= strlen(y))...
if(strlen(x) -strlen(y) >=0)...
但事实上它们是不相同的,第一个语句会是正确的,但第二个语句的结果永远为真,因为strlen的结果是个无符号数,所以操作符 >= 左边的表达式也将是无符号数,而无符号数绝不可能是负的。
 
char *strcpy(char *dst, charconst *src);
由于dst参数将进行修改,所以它必须是个字符数组或者是一个指向动态分配内存的数组的指针不能使用字符串常量。即使新的字符串比dst原先的内容短,由于新字符串是以NUL字节结尾,所以老字符串最后剩余的几个字符也会被有效地删除。另外,必须确保目的字符数组的空间中以容纳需要复制的字符串,如果字符串比数组长,多余的字符仍被复制,它们将覆盖原先存储于数组后面的内存空间的值,这是危险的。
 
char *strcat(char *dst, charconst *src);
这个也需要考虑目标参数的可修改性与长度。它找到dst字符串的末尾,并把src字符串的一份拷贝添加到这个位置。
 
    strcat(strcpy(dst, a), b);
等同于下面两句:
    strcpy(dst, a);
    strcat(dst, b);
 
 
int strcmp(charconst *s1, charconst *s2);
s1小于s2返回小于零,大于返回大于零,相等返回零,所以下面的条件表达式是错误的:
if(strcmp(a,b))
 
char *strncpy(char *dst, charconst *src, size_t len);
它总是正好向dst写入len个字符如果strlen(src)的值小于lendst数组就用额外NUL字节填充到len长度如果strlen(src)的值大于或等于len,那么只有len个字符被复制到dst中,此时它的结果将不会以NUL字节结尾,所以,在使用不受限的函数之前,你首先必须确定字符串实际上是以NUL字节结尾的,确保以后可以将字符串用在不受限函数中,如:
    char buffer[BSIZE];
    strncpy(buffer, name, BSIZE);
    buffer[BSIZE - 1] = '\0';
如果name的内容可以容纳于bufer中,最后那个赋值没有任何效果,但是,如果name太长,这条赋值语句可以保证buffer中的字符串是以NUL结尾的,以后对这个数组使用strlen或其他不受限制的字符串函数将能够正确的工作。
 
char *strncat(char *dst, charconst *src, size_t len);
尽管strncat也是一个长度受限制的函数,但它和strncpy不同,它从src中最多复制len个字符到目标数组的后面,但是,strncat总是在结果字符串后面添加一个NUL字节,而且它不会像strncpy那样对数组用NUL字节进行填充strncat最多向数组复制len个字符(再加一个结尾的NUL字节),它不管目标参数除去原先存在的字符串之后留下的空间够不够。
 
char *strchr(charconst *str, int ch);
char *strrchr(charconst *str, int ch);
第一个从前往后找,第二个从后往前找,返回第一次出现的字符位置。如果不存在就返回NULL
 
char *strpbrk(charconst *str, charconst * group);
这个函数返回一个指向str中第一个匹配group中任何一个字符的字符位置,如果不存在,返回NULL
 
char *strstr(charconst *s1, charconst *s2);
s1中查找出现s2子串,如果不存在返回NULL,如果第二个是一个空字符串,则返回s1
 
函数库中没有strrstr函数,我们自已来实现一个:
/*
** 在字符串s1中查找字符串s2最右出现的位置,并返回一个指向该位置的指针
*/
#include<string.h>
 
char *
my_strrstr(charconst *s1, charconst *s2) {
    registerchar *last;
    registerchar *current;
 
    /*
     ** 将指针初始化为我们已经找到的前一次匹配位置
     */
    last = NULL;
 
    /*
     ** 只在第2个字符串不为空时才进行查找,如果s2为空,返回null
     */
    if (*s2 != '\0') {
       /*
        ** 查找s2s1中第一次出现的位置
        */
       current = strstr(s1, s2);
 
       /*
        ** 每次找到字符串时,指向它的起始位置,然后查找该字符串下一个匹配位置。
        */
       while (current != NULL) {
           last = current;
           current = strstr(last + 1, s2);
       }
    }
    /*
     ** 返回指向我们找到的最后一次匹配的起始位置的指针
     */
    return last;
}
 
查找一个字符串前缀:
size_t strspn(charconst *str, charconst *group);
返回str起始部分匹配group中任意字符的字符数。例如,如果group包含了空格、制表符等空白字符,这个函数将返回str起始部分空白字符数目。下面的代码将计算一个指向字符串中第一个非空白字符的指针:
    char * ptr = buffer + strspn(buffer, "\n\r\f\t\v");
 
size_t strcspn(charconst *str, charconst *group);
strcspn正好与strspn相反,它对str字符串起始部分中不与group中任何字符匹配的字符进行计数。
示例:
    char buffer[] = "25,142,330,Smith,J,239-4123";
    int len1, len2;
    len1 = strspn(buffer, "0123456789");
    printf("%d\n", len1);//2
    len2 = strcspn(buffer, "ith");
    printf("%d\n", len2);//13
 
查找标记:
char * strtok(char * str,constchar *set)
strtok函数使用set字符串中的标记将字符串str分隔成不同的独立部分,每次可以返回这个独立部分。strtok函数找str中的标记后,使用NUL替换,并返回一个指向标记前的邻近字符串指针。如果函数的第一个参数str不为NULL,函数将查找字符串的第一个标记,如果为NULL,函数就从上一次查找的位置后开始查找标记。通常,第一次调用时传递一个字符串指针,以后,这个函数被重复调用(第一参数传递NULL),直到它返回NULL
void print_tokens(char *line) {
    staticchar whitespace[] = " ,";
    char *token;
    for (token = strtok(line, whitespace); token != NULL; token = strtok(NULL,
           whitespace)) {
       printf("Next token is %s\n", token);
    }
}
int main() {
    char buffer[] = "a b,c";
    print_tokens(buffer);
}
由于strtok函数保存了处理过的函数的局部状态信息,所以不能同时解析两个字符串(也不能用在多线程中)。因此,如果for循环体内调用了一个在内部调用strtok函数,上面的程序将不能正常执行。另外,该函数会修改它所处理的字符,如果源字符串不能被修改,那就先拷贝一份,再将这份拷贝传递给该函数。
 
全局错误码所对应的描述信息
char *strerror(int error_number);
当你调用一些函数,请求操作系统执行一些功能,如果出现错误,操作系统是通过设置一个外部整型变量errno进行错误代码报告的。而strerror函数把其中一个错误代码作为参数并返回一个指向用于描述错误的字符串的指针。
    printf("%s\n", strerror(1));//Operation not permitted
    printf("%s\n", strerror(2));//No such file or directory
    printf("%s\n", strerror(3));//No such process
 
内存函数
内存函数可以处理任何字节,包括字符串中的NUL字节,所以如果某个字符串中含有NUL字节,就需要使用内存函数来处理。
void *memcpy(void *dst, voidconst *src, size_t length);
void *memmove(void *dst, voidconst *src, size_t length);
void *memcmp(voidconst *a, voidconst *b, size_t length);
void *memchr(voidconst *a, int ch, size_t length);
void *memset(void *a, int ch, size_t length);
每个原型最后一个参数表示需要处理的字节数。与strn开头的函数不同,它们在遇到NUL字节时并不会停止操作。这些函数可以用于任何类型,因为参数为void*型指针,而任何类型的指针都可以转换为void*类型指针。对于长度大于一个字节的数据(如拷贝整型数组),要确保把数量和数据类型的长度相乘:
    memccpy(saved_answers, answers, count * sizeof(answers[0]));
你也可以使用这种技巧复制结构或结构数组。
 
memmove的行为与memcpy差不多,只是它的源和操作数的内存可以重叠,它可能比 memcpy慢一些,但是,如果源和目标参数可能存在重叠,就应该使用 memmove
    int a[5] = { 1, 2, 3, 4 };
    memmove(a + 1, a, 4 * sizeof(a[0])); 
 clip_image002[36]
 
memcmp按照无符号字符逐字节进行比较,函数的返回类型和strcmp函数一样。由于这些值是根据一串无符号字节进行比较的,所以如果用于比较的不是单字节的数据如整数或浮点数就可以能出现不可预料的结果。
 
memchrstrchr相似,不过可以查找NULl字节以后的内容,并返回一个指向该位置的指针。
    char a[] = { 'a', 0, 'b' };
    printf("%c\n", strchr(a, 'b') == NULL ? '0' : *strchr(a, 'b'));//0
    printf("%c\n", memchr(a, 'b', 3) == NULL ? '0'
           : *(char *) memchr(a, 'b', 3));//b
 
memset函数把从aga vck r length个字节都设置为字符值ch,例如:
    memset(buffer, 0, SIZE);
buffer的前个字节都初始化为0
 
posted @ 2015-01-28 21:51  江正军  阅读(681)  评论(0编辑  收藏  举报