C和指针学习笔记上剖析.docx
- 文档编号:29249546
- 上传时间:2023-07-21
- 格式:DOCX
- 页数:17
- 大小:24.84KB
C和指针学习笔记上剖析.docx
《C和指针学习笔记上剖析.docx》由会员分享,可在线阅读,更多相关《C和指针学习笔记上剖析.docx(17页珍藏版)》请在冰豆网上搜索。
C和指针学习笔记上剖析
第一章指针
●内存和变量:
内存中的每个位置由一个独一无二的地址标识,并且内存中每个位置都
包含一个值。
高级语言所提供的特性之一就是通过名字而不是地址来访问内存的位置。
这些名字就是我们所称的变量。
●值和类型:
不能简单地通过检查一个值的位来判断它的类型,值得类型并非值本身所
固有的一种特性,而是取决于它的使用方式。
●指针变量的内容:
指针的初始化是用&操作符完成的,它用于产生操作数的内存地址。
例如:
♓⏹♦♋=112;
♓⏹♦✉♌=&♋;
说明:
♌的值应该是操作数♋所在的地址。
●间接访问操作符:
通过一个指针访问它所指向的地址的过程称为间接访问或解引用指
针。
例如:
♓⏹♦✉♌=&♋;
说明:
✉♌为操作数♋所在地址中的值,✉是用于执行间接访问的单目操作符。
●末初始化和非法的指针
例如非法操作:
♓⏹♦✉♋;
⑤
✉♋=12;
说明:
究竟♋指向哪里?
我们声明了这个变量,但从未对它进行初始化,所以我们没有办法预测12这个值将储存于什么地方。
●☠✞☹☹指针:
标准定义了☠✞☹☹指针,它作为一个特殊的指针变量,表示不指向任何
东西。
从定义上看,☠✞☹☹指针并未指向任何东西。
因此,对一个☠✞☹☹指针进行解引用操作是非法的。
在对指针进行解引用操作之前,你首先必须确保它并非☠✞☹☹指针。
●指针,间接访问和左值:
指针变量可以作为左值,并不是因为它们是指针,而是因为
它们是变量。
间接访问指定了一个特定的内存位置,这样我们可以把间接访问表达式的结果作为使用。
●指针,间接访问和变量
例如:
✉&♋=25;
说明:
首先,&操作符产生变量♋的地址,它是一个指针变量。
接着,✉操作符访问其操作数所表示的地址。
所以♋的值为25。
●指针常量
例如:
✉100=25;
说明:
这条语句是非法的,因为字面值100的类型是整型,而间接访问操作只能作用于指针类型表达式。
所以可以改成如下语句。
例如:
✉☎♓⏹♦✉100=25;
说明:
这个技巧唯一有用之处是你偶尔通过地址访问内存中某个特定的位置,它并不是用于访问某个变量,而是访问硬件本身。
●指针的指针
c
例如:
inta=12;
int*b=&a;
int**c=&b;
说明:
变量b是一个“指向整型的指针”,所以c是指向“指向整型的指针”的指针。
如上图所示。
表1双重间接访问
表达式
相当的表达式
a
12
b
&a
*b
a,12
c
&b
*c
b,&a
**c
*b,a,12
●实例
1.计算一个字符串的长度
#include
size_t
strlen(char*string
{
intlength=0;
while(*string++!
=’\0’
length+=1;
returnlength;
}
2.在一组字符串中查找
#include
#defineTURE1
#defineFALSE0
int
find_char(char**strings,charvalue
{
char*string;
while((string=*strings++!
=NULL{
while((*string!
=’\0’{
if(*string++=value
returnTURE;
}
}
returnFALSE;
}
}
●指针运算:
指针加上一个整数的结果是另一个指针。
问题是,它指向哪里?
如果你将
字符指针加1,运算结果产生的指针指向内存中的下一个字符。
当一个指针和一个整数量执行算数运算前始终会根据合适的大小进行调整。
这个“合适的大小”就是指针所指向类型的大小。
例如:
float占据4个字节,在计算float型指针加3的表达式时,这个3将根据float类型的大小(此例中为4)进行调整(相乘)。
这样,实际加到指针上的整型值为12。
C的指针算数运算只限于两种形式:
1.指针±整数:
标准定义这种形式只能用于指向数组中某个元素指针。
2.指针—指针:
只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。
指针减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。
C的关系运算:
用下列关系操作符对两个指针值进行比较是可能的:
<,<=,>,>=
不过前提是它们都指向同一个数组中的元素。
例1:
#defineN_VALUES5
floatvalues[N_VALUES];
float*vp;
for(vp=&values[0];vp *vp++=0; 说明: 这个测试是合法的,因为vp和指针常量都指向同一数组中的元素。 例2: for(vp=&values[N_VALUES];vp>&values[0]; *--vp=0; 说明: 数组元素将以相反的次序清除。 我们让vp指向数组最后那个元素后面的内存位置,但在对它进行间接访问之前先执行自减操作。 例3: for(vp=&values[N_VALUES-1];vp>=&values[0];vp-- *vp=0; 说明: 在数组第1个元素被清除之后,比较表达式vp>=&values[0]的值是未定义的,因为vp移到了数组的边界之外。 标准允许指向数组元素的指针与指向数组最后一个元素的后面的那个内存位置的指针进行比较,但不允许与指向数组第1个元素之前的那个内存位置的指针进行比较。 第二章函数 ●函数定义: 函数的定义就是函数体的实现。 函数体就是一个代码块,它在函数被调用时执行。 函数定义的语法: 类型 函数名(形式参数) 代码块 程序例子: 在数组中寻找某个特定整型值的存储位置,并返回一个指向该位置的指针 #include int* find_int(intkey,intarray[],intarray_len { inti; for(i=0;i if(array[i]==key return&array[i]; returnNULL; } ●return语句: 当执行流到达函数定义的末尾时,函数就将返回(return)。 return语句允许你从函数体的任何位置返回,并不一定要在函数体的末尾。 语法: returnexpression; 没有返回值的函数应该把函数的类型声明为void,expression就被省略。 真函数是从表达式内部调用的,它必须返回一个值,用于表达式的求值,这类函数的return语句必须包含一个表达式。 通常,表达式的类型就是函数声明的返回类型。 在C中,子程序不论是否存在返回值,均被称为函数。 调用一个真函数,但不在任何表达式中使用这个返回值是完全可能的。 在这种情况下,返回值就被丢弃。 但是,从表达式内部调用一个过程类型的函数(无返回值)是一个严重的错误,因为这样一来在表达式的求值过程中会使用一个不可预测的值(垃圾)。 ●函数声明: 函数声明出现在函数被调用的地方,函数声明向编译器提供该函数的相关信息,用于确保函数被正确地调用。 原型: 有两种向编译器提供一些关于函数的特定信息的方。 第一种方法,首先,如果同一源文件的前面已经出现了该函数的定义,编译器就会记住它的参数数量和类型,以及函数的返回值类型。 接着,编译器便可以检查该函数的所以后续调用(在同一个源文件中),确保它们是正确地。 第二种方法是使用函数原型。 原型总结了函数定义的起始部分的声明,向编译器提供有关该函数应该如何调用的完整信息。 使用原型最方便(且最安全)的方法是把原型置于一个单独的文件,当其他源文件需要这个函数的原型时,就使用#include指令包含该文件。 函数的缺省认认定: 当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型值。 对于那些不返回整型值的函数,这种认定可能会引起错误。 所以,所有的函数都应该具有原型,尤其是那些返回值不是整型的函数。 例如: 假设有一个函数xyz,它返回float值3.14。 在SunSparc工作站中,用于表示这个浮点数的二进制位模式如下: 010********* 现在假定函数是这样被调用的: floatf; ... f=xyz(; 说明: 如果在函数调用之前编译器无法看到它的原型,它便认定这个函数返回一个整型值,并产生指令将这个值转换成float,然后再赋值给变量f。 转换指令把它们解释为整型值1,078,523,331,并把这个值转换为float类型,结果存储于变量f中。 ●函数的参数: C的规则很简单: 所有参数都是传值调用。 但是如果被传递的参数是一个数组名,并且在函数中使用下标引用该数组的参数,那么在函数中对数组元素进行修改实际上修改的是调用程序中的数组元素。 函数将访问调用程序的数组元素,数组并不会被复制。 这个行为被称为“传址调用”。 数组参数的行为似乎与传值调用规则相悖。 但是,其实并无矛盾,数组名的值实际上是一个指针,传递给函数的就是这个指针的一份拷贝。 下标引用实际上是间接访问的另一种形式它可以对指针执行间接访问操作,访问指针指向的内存位置。 参数(指针)实际上是一份拷贝,但在这份拷贝上执行间接访问操作所访问的是原先的数组。 ●ADT和黑盒: C可以用于设计和实现抽象数据类型(ADT,abstractdatatype),因为它可以限制函数和数据定义的作用域。 这个技巧也被称为黑盒设计。 抽象数据类型的基本想法是很简单的—模块具有功能说明和接口说明,前者说明模块所执行的任务,后者定义模块的使用。 但是模块的用户并不需要知道模块实现的任何细节,而且除了那些定义好的接口之外,用户不能以任何方式访问模块。 限制对模块的访问时通过static关键字的合理使用实现的,它可以限制对那些并非接口的函数和数据的访问。 ●递归 ●可变参数列表 第三章数组 ●一维数组 数组名 inta; intb[10]; 我们把变量a称为标量,因为它是一个单一的值,这个变量的类型是一个整数。 我们把变量b称为数组,因为它是一些值的集合。 下标和数组名一起使用,用于标识该集合中某个特定的值。 b[4]的类型是整型,但b的类型呢? 在C中,几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。 它的类型取决于数组元素的类型: 如果它们是int类型,那么数组名的类型就是“指向int的常量指针”。 只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量。 注意这个值是指针常量而不是指针变量,你不可以修改常量的值。 只有在两种场合下,数组名并不用指针常量来表示——就是当数组名作为sizeof操作符或单目操作符&的操作数时。 sizeof返回整个数组的长度而不是指向数组的指针的长度。 取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量值的指针。 例如: inta[10]; intb[10]; int*c; ... c=&a[0]; 说明: 表达式&a[0]是一个指向数组第一个元素的指针。 但那时数组名本身的值,所以下面这条赋值语句和上面那条赋值语句所执行的任务是一样的: c=a。 另外记住,在这里a是常量,不可修改,所以a=c是非法的。 下标引用 例如: *(b+3 说明: 首先,b的值是一个指向整型的指针,所以3这个值根据整型值的长度进行调整。 加法运算的结果是另一个指向整型的指针,它所指向的是数组的第1个元素向后移3个整型长度的位置。 然后,间接访问操作这个新位置,或者取得那里的值(右值),或者把一个新值存储于该处(左值)。 它和下标引用的执行过程完全相同,除了优先级之外,下标引用和间接访问完全相同。 例1: array[subscript] *(array+(subscript 上述两个表达式是相同的。 例2: intarray[10]; int*ap=array+2; 说明: 在进行指针加法运算时会对2进行调整。 运算结果所产生的指针ap指向array[2]。 根据上面的程序,写出关于涉及ap的表达式所对应的关于array的对等式。 关于♋❒❒♋⍓的对等式 关于♋❒❒♋⍓的对等式 ♋☐ &♋❒❒♋⍓☯2] ✉♋☐ ♋❒❒♋⍓☯2] ♋☐☯0] ✉☎♋☐+☎0=♋❒❒♋⍓☯2] ♋☐+6 ♋❒❒♋⍓+8或&♋❒❒♋⍓☯8] ✉♋☐+6 ♋❒❒♋⍓☯2]+6 ✉☎♋☐+6 ♋❒❒♋⍓☯8] ♋☐☯6] ✉☎♋☐+☎6=♋❒❒♋⍓☯8] &♋☐ 合法但无法预测♋☐位置 ♋☐☯-1] ✉☎♋☐+☎-1=♋❒❒♋⍓☯1] ♋☐☯9] ♋❒❒♋⍓☯11]但是非法 另外: 2[array]=*(2+(array=*(array+2,2[array]这个诡异技巧之所以可行,是缘于C实现下标的方法。 指针与下标 如果你可以互换的使用指针表达式和下标表达式,那么你应该使用哪一个? 下标更容易理解,尤其是在多维数组中。 但是下标绝不会比指针更有效率,但指针有时候会比下标更有效率。 例1: intarray[10],a; for(a=0;a<10;a+=1 array[a]=0; 例2: intarray[10],*ap; for(ap=array;a *ap=0; 说明: 上面两个程序都执行了将array数组清零的任务。 例1中,为了对下标表达式求值,编译器在程序中插入指令,取a的值,并把它与整型的长度4相乘。 这个乘法需要花费一定的时间和空间。 例2中,循环每次执行时,执行乘法运算的数都是两个相同的数(1和4)。 结果,这个乘法只在编译时执行一次——程序现在包含了一条指令,把4与指针相加。 程序运行时并不执行乘法运算。 这个例子说明了指针比下标更有效率的场合——当你在数组中1次1步地移动时,与固定数字相乘的运算在编译时完成,所以运行时所需的指令就少一些。 例3: a=get_value(; array[a]=0; 例4: a=get_value(; *(array+a=0; 说明: 上面两个例子所产生的代码并无区别。 a可能是任何值,在运行时方知。 所以两种方案都需要乘法指令,用于对a的值进行调整。 这个例子说明了指针和下标的效率完全相同的场合。 指针的效率 1.当你根据某个固定数目的增量在一个数组中移动时,使用指针变量将比使用下标产生效率更高的代码。 当这个增量是1并且机器具有地址自动增量模式时,这点表现得更为突出。 2.声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高。 3.如果你可以通过测试一些已经初始化并经过调整的内容来判断循环是否应该终止,那么你就不需要使用一个单独的计数器。 4.那些必须在运行时求值的表达式较之如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高。 数组和指针 例如: inta[5]; int*b; 说明: 声明一个数组时,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置。 声明一个指针变量时,编译器只为指针本身保留内存空间,如果它是一个自动变量,它甚至根本不会被初始化。 因此,上述声明之后,表达式*a是完全合法的,但表达式*b却是非法的。 *b将访问内存中某个不确定的位置,或者导致程序终止。 另一方面b++可以通过编译,但a++不可以,因为a的值是常量。 作为函数参数的数组名 例如: voidstrcpy(char*buffer,charconst*string { while((*buffer++=*string++! =’\0’; } 说明: while语句中的*string++表达式,它取得string所指向的那个字符,并且产生一个副作用,就是修改string,使它指向下一个字符。 用这种方式修改形参并不会影响调用程序的实参,因为只有传递给函数的那份拷贝进行了修改。 另外关于这个函数,还有两个要点值得一提。 首先,形参被声明为一个指向const字符的指针。 对于一个并不打算修改这些字符的函数而言,预先把它声明为常量,第一,这是一个良好的文档习惯,第二,编译器可以捕捉到任何试图修改该数据的意外错误,第三,这类声明允许向函数传递const参数。 声明数组参数 例如: intstrlen(char*string; intstrlen(charstring[]; 说明: 调用函数时实际传递的是一个指针,所以函数的形参实际上是一个指针。 上面两个函数原型是相等的。 但是第一个指针形式更加准确,因为实参实际实际上是个指针,而不是数组。 函数原型中的一维数组形参无需写明它的元素数目,因为函数并不为数组参数分配内存空间。 形参只是一个指针,它指向的是已经在其他地方分配好内存的空间。 初始化 1.静态初始化: 数组初始化的方式类似于标量变量的初始化方式——也就是取决于它们的存储类型。 存储于静态内存的数组只初始化一次,也就是在程序开始执行之前。 程序并不需要执行指令把这些值放到合适的位置,它们一开始就在那里了。 如果数组未被初始化,数组元素的初始值将会自动设置为零。 当这个文件载入到内存中准备执行时,初始化后的数组值和程序指令一样也被载入到内存中。 因此当程序执行时,静态数组已经初始化完毕。 2.自动初始化: 因为自动变量位于运行时堆栈中,执行流每次进入它们所在的代码块时,这类变量每次所处的内存位置可能并不相同。 在程序开始之前,编译器没有办法对这些位置进行初始化。 所以,自动变量在缺省情况下是未初始化的。 如果自动变量的声明中给出了初始值,每次当执行流进入自动变量声明所在的作用域时,变量就被一条隐式的赋值语句初始化。 这条隐式的赋值语句和普通的赋值语句一样需要时间和空间来执行。 数组的问题在于初始化列表中可能有很多值,这就可能产生许多条赋值语句。 对于那些非常庞大的数组,可能它的初始化时间可能非常可观。 因此,这里就需要权衡利弊。 当数组的初始化局部与一个函数(或代码块)时,在程序的执行流每次进入该函数(或代码块)时,每次都对数组进行重新初始化是不是值得。 如果答案是否定的,你就把数组声明为static,这样数组的初始化只需在程序开始前执行一次。 不完整的初始化 例如: intvector[5]={1,2,3,4,5,6}; intvector[5]={1,2,3,4}; 说明: 在这两种情况下,初始化值的数目和数组元素的数目并不匹配。 第1个声明是错误的,我们没有办法把6个整型值装到5个整型变量中。 但是,第2个声明是合法的,它为数组前4个元素提供了初始值,最后一个元素则初始化为0。 自动计算数组长度 例如intvector[]={1,2,3,4,5}; 说明: 如果声明中并未给出数组的长度,编译器就把数组的长度设置为刚好能够容纳所有的初始值的长度。 如果初始值列表经常修改,这个技巧尤其重要。 字符数组的初始化 例如: charmessage[]={‘H’,’e’,’l’,’l’,’o’,0}; charmessage[]=”hello”; 说明: 第二条程序,尽管看上去它像是一个字符串常量,实际上并不是。 它是前例的初始化列表的另一种写法。 当用于初始化一个字符数组时,它就是一个初始化列表。 在其他任何地方,它都表示一个字符串常量。 例如: charmessage1[]=”Hello”; char*message2=”Hello”; 说明: 前者初始化一个字符数组的元素,而后者则是一个真正的字符串常量。 这个指针变量被初始化为指向这个字符串常量的存储位置。 ●多维数组 例如: inta; intb[10]; intc[6][10]; intd[3][6][10]; 说明: a是个简单整数;b是个向量,包含10和整数;c只是在b的基础上再加一维,可以把c看作是一个包含6个元素的向量,只不过它的每个元素本身是一个包含10个整形元素的向量,即c是一个一维数组的一维数组;d本身是一个包含3个元素的数组,每个元素都是包含6个元素的数组,而这6个元素中的每一个又都是包含10个整型元素的数组。 简洁的说,d是一个3排6行10列的整型三维数组。 存储顺序: 在C中,多维数组的元素存储顺序按照最右边的下标率先变化的原则,称为行主序。 数组名: 一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第一个元素。 而多维数组的第1维的元素实际上是一个数组。 例如: intmatrix[3][10]; 说明: 上面的声明创建了matrix,它可以看作是一个一维数组,包含3个元素,只是每个元素恰好是包含10个整型元素的数组。 matrix这个名字的值是一个指向它第一个元素的指针,所以matrix是一个指向一个包含10个整型元素数组的指针。 下标: matrix 例如: intmatrix[3][10]; matrix[1][5] matrix matrix+1 说明: 如上图所示matrix的类型是“指向包含10个整型元素的数组的指针”,它指向包含10个整型元素的第1个子数组。 matrix+1也是一个“指向包含10个整型元素的数组的指针”,但它指向matrix的另一行。 因为1这个值根据包含10个整型元素的数组的长度进行调整,所以它指向matrix的下一行。 *(matrix+1: 事实上标识了一个包含10个整型元素的子数组。 数组名的值是一个常量指针,它指向数组的第1个元素。 *(matrix+1+5: 前一个表达式是个指向整型值的指针,所以5这个值根据整型的长度进行调整。 整个表达式的结果是一个指针,它指向的位置比原先那个表达式所指向的位置向后移动了5个整型元素。 *(*(matrix+1+5: 它所访问的正是图中的matrix[1][5]。 如果它作为右值使用,你就取得存储于那个位置的值。 如果它作为左值使用,这个位置将存储一个新值。 上面这个表达式与*(matrix[1]+5、matrix[1][5]相同。 第四章结构和联合 ●结构基础知识 结构声明: structtag{member-list}variable-list; 结构成员: 结构成员的直接访问: 结构成员的间接访问: 结构的自引用: 不完整的声明: 结构的初始化: 链表: 节点的集合。 链表中的每个节点通过链或指针连接在一起。 程序通过指针 访问链表中的节点。 ●单链表: 在单链表中,每个节点包含一个指向链表下一节点的指针。 根指针(rootpointer)指向链表第1个节点,找到根指针后,指针就可以带你访问剩余所有的节点。 最后一个节点中的指针为NULL指针。 typedefstructNODE{ structNODE*link;//节点中的指针,指向下一个节点; intvalue;//节点中的数据为整型值; } 在链表的起始位置插入一个节点(函数必须修改根指针) 方法1: 由于函数不能访问变量root,可把root声明为全局变量,这样插入函数就可以修改它。 这是最容易但是最坏的解决方法。 因为,函数只对这个链表起作用。 方法2: 把一个指向root的指针作为参数传递给函数。 root是一个指向Node的指针,所以参数的类型应该是Node**,也就是指向Node指针的指针。 malloc(size): 向系统申请分配指定size
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 指针 学习 笔记 剖析