《C和指针》学习

2013. 8.29---2013. 9.1---待续

前五章查阅用,没看。

6.13 指针运算

当一个指针和一个整数量执行算术运算时,整数在执行加法运算前始终会根据合适的大小进行调整。这个“合适的大小”就是指针所指向类型的大小,“调整”就是把整数值和“合适的大小”相乘。例如:某台机器上,float占据4个字节。在技术float型指针加3的表达式时,这个3将根据float类型的大小进行调整(相乘)。这样,实际加到指针上的整型值为12。 

算术运算:

形式一:指针+/-整数

形式二:指针—指针

只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。

运算结果是将指针差值除以数组元素类型的长度,这种对差值的调整使得指针的运算结果与数据的类型无关。

关系运算:

可使用<, <=, >, >=,使用的前提是它们都指向同一个数组中的元素。

例如: 

for(vp=&values[0]; vp< &values[N_VALUE]; )
指针总结:

我们必须使用间接访问来获得它所指向位置存储的值。声明一个指针变量并不会自动分配任何内存。在对指针指向间接访问前,指针必须进行初始化,或者使它指向现有的内存,或者给他分配动态内存。对未初始化的指针变量执行间接访问操作是非法的,而且这种错误长城难以检测。

对任何并非指向数组元素的指针执行算术运算是非法的。如果一个指针减去一个整数后,运算结果产生的指针所指向的位置在数组第一个元素之前,那么它是非法的。

加法运算稍有不同,如果结果指针指向数组最后一个元素后面的那个内存位置仍是合法(但是不能对这个指针指向间接访问操作),不过再往后就不合法了

第七章  函数

如果函数无需想调用程序返回一个值,这类函数在绝大多数其他语言中被称为过程(procedure)。

7.1,7.2  函数定义/声明‘

函数声明更好方法:用头文件包含函数原型。

(1)现在函数原型具有文件作用域,所以原型的一份拷贝可以作用于整个源文件,较之在该函数每次调用前单独书写一份函数原型要容易得多。

(2)现在函数原型只书写一次,不会出现多份原型的拷贝之间的不匹配现象。

(3)如果函数的定义进行了修改,只需要修改原型,并重新编译所有包含了该原型的源文件即可。

(3)如果函数的原型同时也被#include指令包含到定义函数的文件中,编译器就可以确认函数原型与函数定义的匹配。

注:如果函数没有参数,声明应写作:

int *func(void);

7.3函数的参数 7.4ADT(abstract data type)和黑盒  7.5 递归

C语言的所有参数均以“传值调用”方式进行传递。

如果被传递的参数是一个数组名,并且在函数中使用下标引用该数组的参数,那么在函数中对数组元素进行修改实际上修改的是调用程序中的数组元素。函数将访问调用程序的数组元素,数组并不会被复制。这个行为被称为“传址调用”。实际上数组名的值是一个指针,传递给函数的就是这个指针的一份拷贝,但是在这份拷贝上执行间接访问操作所访问的是原先的数组。

两个规则:

(1)传递给函数的标量参数是传值调用的。

(2)传递给函数的数组参数在行为上就像它们是通过传址调用那样。

ADT:抽象数据类型。C可以用于设计和实现ADT,因为它可以限制函数和数据定义的作用域。这个技巧也被称为黑盒设计。

模具具有功能说明和接口说明。前者说明模具所执行的任务,后者定义模具的使用。但是,模块的用户并不需要知道模块实现的任何细节。而且除了定义好的接口之外,用户不能以任何方式访问模块。

限制对模块的访问是通过static关键字的合理使用实现的。它可以限制那些并非接口的函数和数据的访问。

递归的两个特性:(1)存在限制条件,当符合这个条件时,递归便不再继续。

                                (2)每次递归调用之后越来越接近这个限制条件。

注意:递归函数调用将涉及一些运行时的开销——参数必须压到堆栈中,为局部变量分配内存空间(所有递归均如此),寄存器的值必须保存等。

             许多问题是以递归形式进行解释的,这只是因为它比非递归形式更为清晰。但是这些问题的迭代实现往往比递归实现效率更高,虽然代码可读性差了点。当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性可以补偿它所带来的运行时开销。

            许多教科书把阶乘和斐波那契数列用来说明递归,这是很不幸的,因为递归并没有提供任何优越之处,比如斐波那契数列,在计算F(10)时,F(3)的值被计算了21次,计算F(30)时,F(3)被计算了317811次,这么多次的计算所产生的结果是完全一样的,除了其中之一外,其余都是浪费,这个额外开销相当恐怖。

7.6 可变参数列表

stdarg宏:定义于 stdarg.h 头文件。其中声明了一个类型 va_list和三个宏——va_start、va_arg、va_end。

例程:

//可变参数限制,计算指定数量的值的平均值
#include "stdarg.h"
float average( int n_value, ...  )
{
	va_list var_arg;
	int count;
	float sum=0;
	va_start( var_arg, n_value);
	//添加取自可变参数列表的值
	for( count=0; count<n_value; count++ )
	{
		sum+= va_arg( var_arg, int );
	}
	//完成处理可变参数
	va_end(var_arg);
	return sum/n_value;
}
void main( )
{
    int a1=1,a2=2,a3=3,a4=4,a5=5;
	float ave3=0, ave4=0;
    ave3=average(3, a1, a2, a3 );
	ave4=average(4,a1,a2,a3,a4);
	printf("ave3=%f\nave4=%f\n",sum3,sum4);
}
输出结果为:


第八章  数组

8.1 一维数组

在C中,在几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第1个元素的地址。

数组和指针的区别:数组具有确定数量的元素,而指针只是一个标量值。

指针与下标:在可读性方面,下标有一定的优势,但是下标绝不会比指针更有效率,相反,指针有时会比下标更有效率。

指针效率小节中的对比程序中可以看出:

(1)当根据某个固定数目的增量在一个数组中移动时,使用指针变量将比使用下标产生效率更高的代码。当这个增量是1并且机器具有地址自动增量模型时,这点表现得更加突出。

(2)声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高。

(3)如果你可以通过测试一下已经初始化并经过调整的内容来判断循环是否应该终止,那么你就不需要使用一个单独的计数器。

(4)那些必须在运行时求指的表达式较之诸如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高。

void strcpy( char *buffer , char const *string)
注意:形参被声明为一个指向const字符的指针。对于一个并不打算修改这些字符的函数来说,预先把它声明为常量有何意义呢?

(1)这是一个两个的文档习惯。有些人希望仅观察该函数的原型就能发现该数据不会被修改,而不必阅读完整的函数定义。

(2)编译器可以捕捉到任何试图修改该数据的意外错误。

(3)这类声明允许响函数传递const参数。

8.2 多维数组

matrix[6][10]到底是6行10列还是10列6行呢??

答案是:在某些上下文环境中,两种答案都对。如果你根据下标把数据存放于数组中并在以后根据下标查找数组中的值,那么不管你把第1个下标解释为行还是列,都不会有什么区别。只要你每次都坚持使用同一种方法,这两种解释都是可行的。但是把第1个解释为行还是列并不会改变数组的存储顺序。不能修改内存中数组元素的实际存储方式,这个顺序是由标准定义的。

对于数组名:

int a[10];

int b[6][10];

一维数组名的值  a  是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第1个元素。

多维数组的第一维数组其实是另一个数组。所以多维数组名是一个指向它第1个元素的指针。即:b是一个指向一个包含10个整型元素的数组的指针。

注意:*(b+1)事实上标识了一个包含10个整型元素的子数组。数组名的值是个常量指针,它指向数组的第1个元素。

           *(b+1)+5  其中,5是根据整型的长度进行调整,整个表达式的结果是一个指针,它指向的位置比上一个的位置向右移动了5个整型元素。

            *(*(b+1)+5)访问的是上式指针所指位置的元素值。

实际上:*(b+1)相当于b[1]

                *(b+1)+5相当于  b[1][5]

指向数组的指针:

一维和二维的如下:

int    vector[10],    *vp = vector;

int     matrix [3][10],   (*p)[10];

注意:  {*p}[10],下标引用 的优先级高于间接访问,但由于括号的存在,首先执行的还是间接访问。所以p是个指针,是一个指向整型数组的指针。

初始化:  int  (*p)[10]=matrix;  它使p指向matrix的第1行。

创建一个简单的整形职责,指向matrix的第一个整型元素,以下是两种方法:

    int  *p = &matrix[0][0];

    int   *p=matrix [0];            增加这个指针的值可使它指向下一个整型元素。

一维二维数组作为函数参数的对比

int     vector[10];

。。。

func1 (vector);

参数vector的类型是指向整型的指针,指向数组第一个元素。

int    matrix  [3][10];

。。。

func2 (matrix);

参数matrix的类型是指向包含10个整型元素的数组的指针。 

8.3  指针数组(注意与指向数组的指针区分。)

在对函数进行声明时,由于数组名的值是一个指针,所以无论传递给函数的是指针还是数组名,函数都能运行。

哪个方案更好?这取决于你希望存储的具体字符串。如果它们的长度都差不多,那么矩阵形式更紧凑一下,因为它无需指针。

但是如果各个字符串的长度千差万别,或者更糟,绝大多数字符串都很短,但少数几个却很长,那么指针数组形式就更紧凑一些。它取决于指针所占用的空间是否小于每个字符串都存储于固定长度的行所浪费的空间。

数组总结:

(1)在绝大多数表达式中,数组名的值是指向数组第1个元素的指针。这个规则之一两个例外:sizeof返回整个数组所占用的字节而不是一个指针所占用的字节。单面操作符&返回一个指向数组的指针,而不是一个指向数组第1个元素的指针的指针。

(2)指针和数组并不相等。数组的属性和指针的属性大相径庭。当我们声明一个数组时,它同时也分配了一下内存空间,用于容纳数组元素。但是当我们声明一个指针时,它只分配了用于容纳指针本身的空间。

(3)只要有可能,函数的指针形参都应该声明为const。


第九章  字符串、字符和字节

            字符串就是一串零个或多个字符,并且以一个位模式为全0的NUL字节结尾。(字符串所包含的字符内部不能出现NUL字节,这个限制很少会引起问题,因为NUL字节并不存在与它相关联的可打印字符,这也是它被选为终止符的原因。)NUL不是字符串的一部分,所以字符串的长度并不包括NUL字节。

注意:两个表达式

if(  strlen(x)  >= strlen(y) )....
if(  strlen(x)  - strlen(y)>=0 )....
看似一样,实际上他们是不相等的。

第1条语句会按照预想的工作

第2条语句结果将永远是真。strlen是无符号数,所以>=操作符的左边将是无符号数,绝不会是负数。如果强制转换为int,可以消除问题。

复制字符串      strcpy(“original message”, "A different message");
连接字符串      strcat (”Hello“,“how are you”);
字符串比较      strcmp(char  const  *s1, char  const  *s2);
受限字符串比较  strncmp(  char const *s1, char const *s2 , size_t  len );
受限字符串连接  strncat()
受限字符串比较  strncmp()

和strcmp一样,strncmp把源字符串的字符复制到目标数组。然而它总是正好想dst写入len个字符。

如果strlen(src)的值小于len,dst数组就用额外的NUL字符填充到len长度。

如果strlen(src)的值大于或等于len,那么只有len个字符被复制到dst中。注意:它的结果将不会以NUL字符结尾

因此,使用时:

char buffer[BSIZE];
...
strncpy( buffer, name , BSIZE );
buffer[BSIZE-1] ='\0';
这样就保证了buffer中字符串总是以NUL结尾。

对于strncat:它总是在结果字符串后面添加一个NUL字符,而且它不会像strncpy那样对目标数组进行填充。

9.5 查找

char *strchr ( char const *str, int ch );
char *strrchr( char const *str, int ch );
ch是一个整型值,但是它包含了一个字符值。strchr在字符串str中查找ch第一次出现的位置,找到后函数返回一个指向该位置的指针。不存在就返回NULL指针。

strrchr和strchr基本一致,只是它所返回的是一个指向字符串中该字符最后一次出现的位置。

查找任何几个字符
char *strpbrk ( char const *str, char const *group );
该函数返回一个指向str中第一个匹配group中任何一个字符的字符位置。未找到返回NULL。
查找一个字串
char *strstr ( char const *s1, char const *s2 );
如果没有完整的在s1中出现,就返回NULL,如果s2是个空字符串,函数就返回s1.。

标准中并不存在strrpbrk、strrstr函数,需自己实现。
9.8 字符操作:

iscntrl 任何控制字符
isspace 空白字符:‘’,‘\f’,'' '\n','\r','\t','\v'
isdigit 十进制数字0-9
isxdigit 十六进制数
islower 小写a-z
isupper 大学字母A-Z
isalpha 字母a-z,A-Z或0-9
ispunct 标点符号,任何不属于数字或字母
isgraph 任何图形字符
isprint 任何可打印字符

字符转换

int tolower(int ch);
int toupper(int ch);

注意:

if ( ch>='A'  &&  ch<='Z' )
if (isupper(ch))
上面第一条语句使用ASCII字符集运行。但在使用EBCDIC字符集的机器上将会失败。

第二条都能顺利运行。

9.9 内存操作

void *memcpy ( void *dst , void const *src , size_t  length );
void *memmove ( void *dst , void const *src , size_t  length );
void *memcmp ( void const *a , void const *b , size_t  length );
void *memchr ( void const *a ,int ch, size_t  length );
void *memset ( void *a, int ch, size_t  length );
与strn函数区别:他们遇到NUL字节时并不会停止操作。

memmove和memcpy差不多,只是它的源和目标操作数可以重叠。

第十章 结构和联合

聚合数据类型:能够同时存储超过一个的单独数据。C提供了两种类型的聚合数据类型:数组和结构。

数组是相同类型的元素的集合,它的每个元素是通过下标引用或指针间接访问来选择的。

结构也是一些值的集合,这些值称为它的成员(member),但一个结构的各个成员可能具有不同的类型。
声明:

struct SIMPLE
{
     int   a;
     char   b;
     float   c;
};


标签(tag)字段允许为成员列表提供一个名字,这样它就可以在后续的声明中使用。上面的声明把SIMPLE和这个成员列表联系在一起。该声明并没有提供变量列表,所以它并未创建任何变量。

声明结构时可以使用的另外一个良好的技巧是使用typedef创建一种新的类型:

typedef  struct 
{
     int   a;
     char   b;
     float   c;
}Simple   
这个技巧和声明一个结构标签的效果几乎相同。区别在于Simple现在是个类型名而不是个结构标签,所以后续的声明可能像下面这样子:

Simple   a;
Simple  y[20],  *z;
结构成员的间接访问:

如果你拥有一个指向结构的指针,如何访问成员呢?首先对指针指向间接访问操作,获得这个结构。然后使用点操作符来访问它的成员。

例如:一个函数的参数是个指向结构的指针

void func( struct COMPLEX *cp );
函数访问这个变量所指向的结构的成员f:   
(*cp).f
由于这个概念有点惹人厌,C语言提供箭头操作符->。 例如: cp->f

或者是:使用px->a      时,如果知道这个结构的名字,可以使用功能相同的表达式  x.a  。总之,如果是结构,则可以直接访问,用.操作符;如果是指向该结构体的指针,则使用箭头操作符->。

结构的自引用:

struct  SELF_REF2
{
     int  a;
     struct  SELF_REF2  *b ;
     int  c;
}
注意,自引用时,必须是结构体内部包含一个指向该结构本身的指针。它事实上所指向的是同一种类型的不同结构。一般在更加高级的数据结构链表和树中实现。

警告:
使用自引用时,因为内部要使用该结构名,所以必须使用结构标签来声明b,即 SELF_REF2是不可缺少的,必须先定义。

访问结构成员时,如果想让创建的指向整型的指针:int  *pi  指向结构体的整型成员px.a  

可以表示为:pi = (int  *)px;

但是这个很危险,避开了编译器的类型检查。正确的表达式可以为

pi = &px->a ;   ->操作符优先级高于&操作符,无需括号
10.3 结构的存储分配

系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的的位置。

可以在声明中对结构成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现,这种做法可以最大限度地减少因边界对齐而带来的空间损失。(相同类型放在一起,可以紧挨着存储。)

sizeof操作符能够得出一个结构的整体长度,包括因边界对齐而跳过的那些字节。

如果必须确定结构某个成员的实际位置,应该考虑边界对齐因素,可以使用offsetof宏(定义于stddef.h)

offsetof(type, member );
type就是结构的类型,member就是你需要的那个成员名。表达式结果是个size_t值,表示这个指定成员开始存储的位置距离结构开始存储的位置偏移几个字节。



                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值