C语言基础-指针
6.1 内存和地址
- 计算机的内存由数以亿万的位(bit)构成,每个位可以容纳0或1。
- 现在计算机通常以字节Byte --(8bit)为基本单位,每个字节都有它的地址。
为了存储更大的数据,把多个字节拼在一起,如四个字节构成一个字。
尽管一个字包含了多个字节,但是这个字还是只有一个地址,
至于具体是最高字节的地址还是最低字节的地址来表示,不同的机器有不同的规定。 - 还有一个需要注意的硬件事项:边界对齐 要求边界对齐的机器上整型值的存储位置的起始地址 只能是某些特定的字节,一般是4的整数倍。
- 高级语言提供的特性之一就是通过名字来访问内存的位置而不是地址,这些名字就是变量。
注意: 名字和内存位置之间的关联不是由硬件提供的,而是编译器为我们实现的,实际上实现了变量和地址的关联。但是硬件任然是通过地址访问内存位置。 - 内存中每个字节都有独一无二的地址标识,内存中每一个位置都含有一个值。
- 存储在存储单元的值可以有若干种解释结果,如解释成整形,浮点型等。
所以不能通过一个值的位模式来判断值的类型,值的类型是通过使用方式隐式决定的 如使用它们采用的是整形算术指令还是浮点型。 - 指针变量就是该变量的位置的存储值是一个地址,可通过这个地址访问真正需要访问的数据。
- 通过指针访问它所指向的地址的过程成为间接访问(indirection)或者解引用指针(dereferencing the pointer) ,操作符为单目操作符*。
6.2 未初始化和非法指针
在使用指针进行间接访问之前,一定要确保指针已经初始化和不是NULL指针!
NULL指针表示一个指针变量并不指向任何东西,为零值。
//下面是未初始化使用指针实例,这种写法是错误的。
int *ch;
*ch = 12;
//初始化指针
int a = 12;
int *b = &a;
6.3 指针常量
指针变量只能存储地址不能存储其它类型的值。
* 100 = 20 是错误的写法,100是整型数值,间接访问操作只能作用于指针类型的表达式。正确的写法是: *(int *) 100 = 20;
将100强制转换为指针类型。
需要使用指针常量的地方很少,一般都涉及底层设备,如访问设备通信器通过特定的内存地址读写。
6.4 指针的指针
int a = 21;
int *b = &a;
int **c = &b;//指针的指针
/*
*c == b;
**C == a == *b;
*/
实例:
#include <stdlib.h>
#include <stdio.h?
#define TURE 1
#define FALSE 0
/*
计算一个字符串的长度
*/
int strlen( char *string )
{
int length = 0;
while( *string++ != '\0' ){
length += 1;
}
return length;
}
/*
给定一个指向以NULL结尾的指针列表的指针,在列表中字符串中查找一个特定的字符。
使用指针数组strings来表示一些字符串。
value为我们查找的值。
下面函数不会破坏指针数组。
*/
int find_char1( char **strings, char value )
{
char *string;
while( (string = *strings++) != NULL ){
while( *string != '\0' ){
if( *string++ == value ) return TRUE;
}
}
return FALSE;
}
/*
功能与上类似,但是只能查找一次,会破坏指针数组。
*/
int find_char2( char **strings, char value )
{
assert( strings != NULL );
while( *strings != NULL ){
while( **strings != '\0' ){
if ( *(*string)++ == value ) return TURE;
}
strings++;
}
return FALSE;
}
6.5 指针的运算
算术运算:
- 指针 +/- 整数
- 指针 - 指针 (当两个指针都指向同一个数组的元素时才能相减)
对数组执行加减运算后,如果指针的位置在数组第1个元素之前和最后一个元素之后,那么其效果是未定义的。让指针指向数组最后一个元素后面的的位置是合法的,但执行间接访问可能会失败。
关系运算:
- 只有当指针都指向同一个数组才能比较的关系操作符:< <= > >=
- 没有限制的关系运算符: == !=
注意: 标准允许指向数组元素的指针与指向数组最后一个元素后面的位置的指针进行比较,但和不允许第一个元素之前位置的指针进行比较。
/*
清零数组元素
*/
#define MAX 5
float values[MAX];
float *vp;
//版本一
for( vp = &values[0]; vp < &values[MAX]; ) *vp++ = 0;
//版本二
for( vp = &values[MAX]; vp > &values[0]; ) *--vp = 0;
//下面写法有错误,避免这种写法
for( vp = &values[MAX - 1]; vp >= &values[0];) *vp-- = 0;//vp不允许第一个元素之前位置的指针进行比较。
注意1: 声明一个指针变量并不会自动分配任何内存。在对指针执行间接访问之前必须初始化:要么指向现有的内存,要么分配动态内存。
注意2: 对NULL指针间接访问操作的结果因编译器而异,一般两个结果分别是返回内存零位置的的值和终止程序。
注意3: 对指针值加上或者减去一个整数,原值将乘以指针目标类型长度,切换到目标变量处。
6.6 指向指针的指针的指针
实例:
int i;
int *pi;
int **ppi;
printf( "%d\n", ppi ); //如果ppi是个自动变量,那么它未被初始化,将答应一个随机值。如果是静态变量,打印0.
//ppi未初始化之前,他的存储单元的内容不可知
printf( "%d\n", &ppi ); //将ppi的存储单元的地址打印出来,这个值没什么用
*ppi = 5; //错误,不能对未初始化的指针执行间接访问操作
ppi = π //ppi初始化为指向pi的变量
*ppi = &i; //把pi初始化为指向变量i的指针,pi是通过ppi间接访问得到的。
/*
下面三条语句具有相同的效果
*/
i = 'a';
*pi = 'a';
**ppi = 'a';
int ***pppi; //访问层次还可以加多,只有当确实需要时,才使用多层访问,不然层序变得更大,更慢,更难维护。
6.7 高级声明
提示:程序中的每个函数都位于内存中的某个位置,所以存在只想那个位置的指针是合法的。
实例:
int *f; //把表达式 *f 声明为一个整数。进而推断出f是指向整型的指针。
int *a, b; //这里只有变量a被声明为指针,b为整型变量。
int *k(); //表达式 *K(),先执行函数调用操作符(),因此k是一个函数,它的返回值是一个 指向整型的指针 。
int (*kk)(); //先执行第一个括号聚组,是间接访问在函数调用之前进行。使得kk成为一个函数指针。它所指向的函数返回一个 整型值
int *(*kkk)(); //kkk也是一个 函数指针 ,它所指向的函数返回值是一个 指向整型的指针 。
int *c[]; //先执行下标操作,c为一个数组,它的元素类型是 指向整形的指针 。
int cc()[]; //非法,函数只能返回标量值,不能返回数组
int ccc[](); //非法
int (*x[])(); //先对聚组括号内的表达式求值再函数调用符,所以x是一个元素是 函数指针 的数组。指向的函数的返回值是整型值。
int *(*xx[])(); //xx是一个元素为 函数指针的数组,其指向的函数的返回值为 指向整形的指针。
6.8 函数指针
下面介绍两种常见的函数指针的用途:转换表(jump table)和作为参数传递给另一个函数。
注意1: 函数指针和其他指针一样,对函数指针执行间接访问前一定要把它初始化指向某个函数。
注意2:函数名被使用时总是由编译器把它转换为函数指针。指针指向函数在内存中的位置。然后 函数调用操作符() 调用该函数,执行开始于这个地址的代码
函数初始化实例:
int f( int );
int (*pf)( int ) = &f; //声明并初始化函数指针pf
//这里的&并不需要,函数名被使用时总是由编译器把它转换为 函数指针。&只是显示说明转换操作。
int ans;
/*
下面三条语句作用效果一样
*/
ans = f ( 2 );//函数名f首先被转换为函数指针,指向函数在内存中的位置。然后函数调用操作符调用该函数,执行开始于这个地址的代码
ans = (*pf)( 2 );
ans = pf ( 2 );
6.8.1 回调函数
先看一个之前单向链表查找特定值的函数:
node *search( node *pnode, int const value )
{
while( pnode != NULL ){
if( pnode->value == value )
break;
pnode = pnode->link;
}
return pnode;
}
上面这种实现方法只能实现查找整数的链表。要使查找函数跟查找的类型无关,这样他就能用于任何类型的值的链表。
/*
在一个单链表中查找一个指定的值。它的参数一个是指向聊表的第一个节点的指针。一个指向需要查找的指针,和一个函数指针指向用于比较的函数
*/
node *search( node *pnode, void const *value, int (*compare)( void const *, void const *) )
{
while( pnode != NULL ){
if( compare( &pnode->value, value ) == 0 ) //相等返回0,为了于标准库中的比较函数兼容。
break;
pnode = pnode->link;
}
return pnode;
}
int com_ints( void const *a, void const *b )
{
if( *(int *)a == *(int *)b )
return 0;
else
return 1;
}
//这样调用
target_node = search( root, &target_value, com_ints );
6.8.2 转移表
例子:
switch( oper ){
case add:
result = add( op1, op2 );
break;
case sub:
result = sub( op1, op2 );
break;
...//假设还有很多操作,这里省略掉
//注:把具体操作和用函数实现,实现具体操作和选择操作分隔开是一种良好的代码风格。
}
/*
为了使用switch语句,表示操作符代码oper必须是整数。如果他是从零开始连续的整数,则可以用转换表来实现。
转换表就是一个 函数指针数组
创建转换表需要两步。
1、声明初始化一个函数指针数组,确保这些函数的原型在这个函数指针数组之前声明
2、使用
*/
double add( double, double );
double sub( double, double );
double mul( double, double );
...
double (*oper_func[])( double, double ) = {
add, sub, mul,......
};
result = oper_func[oper]( op1, op2 );
注:使用函数指针数组时,一定要检查数组是否越界。
6.9 命令行参数
在命令行中编写参数来启动一个程序执行,第一个参数就是程序的名字。
6.9.1 传递命令行参数
c程序的main函数有两个形参:
- 第一个成为argc,表示命令行参数的数目。
- 第二个通常称为argv,他指向一组参数,本质上是数组,里面的元素都是指向一个参数文本的指针。
如果程序需要访问命令行参数,main函数就在声明时加上这些参数argc,argv。
一行命令行命令例子:cc -c -o main.c insert.c -o test argc = 7;
cc就是程序的名字,当程序有几组不同的选项进行启动时,程序对第一个参数进行检查,确定是由哪个名字启动的,选择启动项。
/*
一个打印其命令行参数的程序,区分这里和上面的命令行的程序
*/
#include <stdlib.h>
#include <stdio.h>
int main( int argc, char **argv )
{
/*
打印命令行参数,程序名被跳过
*/
while( *++argv != NULL ){
printf( "%s\n", *argv);
}
return EXIT_SUCCESS;
}
6.10 字符串常量
- 当字符串常量出现在表达式中时是个指针常量。
- 而数组名用于表达式中时也是指针常量。
- 我们可以对数组进行下标引用,间接访问,已经指针运算。这些操作对字符串常量也适用。
实例:
"xyz" + 1; //结果是 y
*"xyz" ; //结果是 x
"xyz"[2]; //结果是 z
浙公网安备 33010602011771号