指针
1.指针变量
理解指针的第一步是在机器级上观察指针表示的内容。大多数现代计算机将内存分割为字节,每个字节可以存储8位的信息。
每个字节都有唯一的地址,用来和内存中的其他字节相区别。如果内存中有n个字节,那么可以把地址看作0~n-1的数。
可执行程序由代码(原始C程序中与语句对应的机器指令)和数据(原始程序中的变量)两部分构成。程序中的每一个变量占有一个或多个字节内存,把第一个字节的地址称为变量的地址。下图中,变量i占有地址位2000和2001的两个字节,所以变量i的地址是2000;
而这就是指针的出处。虽然用数表示地址,但是地址的取值范围可能不同于整数的范围,所以一定不能用普通整型变量存储地址。但是,可以用特殊的指针变量存储地址。用指针变量p存储变量i的地址时,我们说p“指向”i。换句话说,指针就是地址,而指针变量就是存储地址的变量。
本书的例子不再把地址显示为数,而是采用更加简单的标记。为了说明指针变量p存储变量i的地址,把p的内容显式位指向i的箭头:
1.1 指针变量的声明
- 对指针变量的声明对普通变量的声明基本一样,唯一的不同就是必须在指针变量名字前放置星号:
- int * p;
- 上述声明说明p是指向int类型对象的指针变量。这里我们用术语对象来代替变量,是因为p可以指向不属于变量的内存区域。
- 指针变量可以和其他变量一起出现在声明中:
- int i,j,a[ ],b[ ], p,q;
- 在这个例子中,i和j都是普通整型变量,a和b是整型数组,而p和q是指向整型对象的指针。
- C语言要求每个指针只能指向一种特定类型的对象:
- int *p;
- double *q;
- char *r;
- 至于引用类型是什么类型则没有限制。事实上,指针变量甚至可以指向另一个指针,即指向指针的指针。
2. 取地址运算符和间接寻址运算符
为使用指针,C语言提供了一对特殊设计的运算符。为了找到变量的地址,可以使用&(取地址)运算符。如果x是变量,那么&x就是x在内存中的地址。为了获得对指针所指向对象的访问,可以使用 (间接寻址)运算符。如果p是指针,那么 p表示p当前指向的对象。
2.1 取地址运算符
声明指针变量是为指针留出空间,但是并没有把它指向对象:
- int * p;
在使用前初始化p是至关重要的。一种初始化指针变量的方法是使用&运算符把某个变量的地址赋给它,或者更常采用左值:
int i, * P;
……
p = &i;
- 上述是一个简单的例子,通过把i的地址赋值给变量p的方法,上述语句把P指向了i:
- 在声明指针变量的同时对它进行初始化时可行的:
- int i;
- *int p = &i;**
- 甚至可以把i的声明和p的声明合并,但是需要首先声明i:
- *int i, p = &i;**
2.2 间接寻址运算符
一旦指针变量指向了对象,就可以使用 * 运算符访问存储在对象中的内容。例如,如果p指向i,那么可以显示出i的值,如下所示:
- *printf("%d\n", p);**
- printf将会显示i的值,而不是i的地址。
-
习惯于数学思维的读者可能希望把 想象成&的运算。对变量使用&运算符产生指向变量的指针,而对指针使用 运算符则可以返回到原始变量。
-
j = * &i;
-
只要p指向i, p就是i的别名。 p不仅拥有和i相同的值,而且对 p的改变也会改变i的值。( p是左值,所以对它赋值是合法的。)下面的例子说明了 * p和i的等价关系,这些图显示了在计算中不同的点上P和i的值。
-
p = &i;
-
i = 1;
-
printf("%d\n",i); //打印1
-
*printf("%d\n", p);** //打印1
-
*p = 2;
-
- - printf("%d\n",i); //打印2
- *printf("%d\n", p);** //打印2
Notice:
不要把间接寻址运算符用于未初始化的指针变量。如果指针变量p没有初始化,那么试图使用p的值会导致未定义的行为,如下:
int *p;
printf("%d",,*p); /wrong
给 *p赋值尤其危险。如果p恰好具有有效的内存地址,下面赋值会试图修改存储在该地址的数据:
int *p;
*p = 1; /wrong
- 如果上述赋值改变的内存单元属于该程序,那么可能会导致出乎意料的行为;如果改变的内存单元属于操作系统,那么很可能会导致系统崩溃。编译器可能会给出警告消息,告知p未初始化,所以请留意收到的警告消息。
3. 指针赋值
C语言允许使用赋值运算符进行指针的复制,前提是两个指针具有相同的类型。假设i,j,p和q声明如下:
- int i,j, p, q;
语句:
- p = &i;
- 这条语句是把P的内容(即i的地址)复制给了q,效果是把q指向了P所指的地方。
这里不太好描述,直接上图
4. 指针作为参数
到目前为止,我们回避了一个十分重要的问题:指针对什么有益呢?因为C语言中指针有几个截然不同的应用,所以此问题没有唯一的答案。在本节中,我们将看到如何把指向变量的指针用作函数的参数。
因为C语言用值进行参数传递,所以在函数调用中用作实际参数的变量无法改变。当函数能够改变变量时,C语言的这种特性就很讨厌了。
指针提供了此问题的解决方法:不再传递变量X作为函数的实际参宿和,而是提供&x,即指向x的指针。声明相应的形式参数p为指针。调用函数时,p的值为&x,因为 p(P指向的对象)将是x的别名。函数体内 p的每次出现都将是对x的间接引用,而且允许函数既可以读取x也可以修改x。为了用实例证明这种方法,下面通过把形式参数int_part和farc_part声明成指针的方法来修改decompose函数。
void decompose(double x,long *int_part,double *frac_aprt);
{
*int_part = (long)x;
*fract_part = x - *int_part;
}
- void decompose(double x,long *int_part,double *frac_part);
- void decompose(double,long *,double *);
- 调用decompose函数
- decompose(3.14159,&i,&d);
- 因为i和d前有取地址符&,所以decompose函数的实际参数时指向i和d的指针,而不是i和d的值。调用decompose函数时,把值3.14159复制到x中,把指向i的指针存储再int_part中,而把指针存储在frac_part中:
-
decompose函数体内的第一个赋值把x的值转换为long类型,并且把此值存储在int_part指向i,所以赋值把值3放到i中:
- 第二个赋值把int_part指向的值(即i的值)取出,现在这个值是3。把此值转换为double类型,并且用x减去它,得到0.14159。然后把这个值存储在farc_part的对象中,当decompose函数返回时,就像原来希望那样,i和d将分别有值3和0.14159。
- 第二个赋值把int_part指向的值(即i的值)取出,现在这个值是3。把此值转换为double类型,并且用x减去它,得到0.14159。然后把这个值存储在farc_part的对象中,当decompose函数返回时,就像原来希望那样,i和d将分别有值3和0.14159。
- 用指针作为函数的实际参数实际上并不新鲜,看下面这个例子:
int i;
……
scanf("%d", &i);
-
必须把&放在i的前面以便给scanf函数传递指向i的指针,指针会告诉scanf函数读取的值放在哪里。如果没有运算符&,传递给scanf函数的将是i的值。
-
虽然scanf函数的实际参数必须是指针,但并不总是需要&运算符,在下面的例子中,我们向scanf函数传递了一个指针变量:
-
int i,*p; …… p = &i; scanf("%d", &p);
- 既然p包含了i的地址,那么scanf函数将读入整数并把它存储在i中。在调用中使用&运算符将是错误的:
- **scanf("%d",&p);**
- scanf函数读入整数并且把它存储在p中而不是i中。
> Notice:
> 向函数传递需要的指针却失败了可能会产生严重的后果。假设我们在调用decompose函数时没有在i和d前面加上&运算符:
- **decompose(3.14159,i,d);**
> decompose函数期望第二个和第三个实际参数是指针,但传入的却是i和d的值。decompose函数没有办法区分,所以它会将把i和d是值当成指针来使用。当decompose函数把值存储到*int_part和*frac_part中时,它会修改未知的内存地址,而不是修改i和d。如果已经提供了decompose函数的原型(当然,应该始终这样做),那么编译器将告诉我们实际参数的类型不对。然而,在scanf的例子中,编译器通常不会检查出传递指针失败,因此scanf函数特别容易出错。
### 4.1 用const保护参数
> 当调用函数并且把指向变量的指针作为参数传入时,通常会假设函数将修改变量(否则,为什么函数需要指针呢?),例如,如果在程序中看到语句
- **f(&x)**
- 大概是希望f改变x的值。但是,f仅需要检查x的值而不是改变它的值也是可能的。指针可能高效的原因是:如果变量需要大量的存储空间,那么传递变量的值会浪费时间和空间。
- 可以使用单词const来表明函数不会改变指针参数所指向的对象。const应放置在形式参数的声明中,后面紧跟着形式参数的类型说明:
~~~C
void f(const int * p)
{
*p = 0; /****WRONG****/
}
- 这一用法表明p是指向“常整数”的指针。试图改变 * p是编译器会检查的一种错误。
5. 指针作为返回值
- 我们不仅可以为函数传递指针,还可以编写返回指针的函数。返回指针的函数是相对普遍的。当给定指向两个整数的指针时,下列函数返回指向两整数中较大数的指针:
int *max(int *a,int *b)
{
if(*a>*b)
return a;
else
return b;
}
- 调用max函数时,用指向两个int类型变量的指针作为参数,并且把结果存储在一个指针变量中:
int *max(int *a,int *b)
{
if(*a>*b)
return a;
else
return b;
}
调用max期间,*a是i的别名,而是*b是j的别名。如果i的值大于j,那么max返回i的地址;否则,max返回j的地址。调用函数后,p可以指向i也可以指向j。
这个例子中max函数返回的指针是可以作为实际参数传入的两个指针中的一个,但这不是唯一的选择。函数也可以返回指向外部变量或指向声明为static的局部变量的指针。
Notice:
//永远不要返回指向自动局部变量的指针:
int *f(void)
{
int i;
……
return &i;
}
//一旦f返回,变量i就不存在了,所以指向变量i的指针将是无效的。有的编译器会有警告。
- 指针可以指向数组元素,而不仅仅是普通变量。设a为数组,则&a[i]是指向a中元素i的指针。当函数的参数中有n个元素,并返回一个指向数组中间元素的指针;
int *find_middle(int a[], int n)
{
return &a[n/2];
}
6. 问与答
- 指针总是和地址一样吗?
答:通常是,但不总是。考虑用字而不是字节划分内存的计算机。字可能包含36位,60位等。如果假设字包含36位,那么内存将有如下显示:
当有字划分内存时,每个字都有一个地址。通常整数占一个字长度,所以指向整数的指针可以就是一个地址。但是,字可以储存多于一个字符。例如,36位的字可以存储6个6位的字符:
或者4个9位的字符:
由于这个原因,可能需要用不同于其他指针的格式存储指向字符的指针。指向字符的指针可以由地址(存储字符的字)加上一个小整数(字符在字内的位置)组成。
- 如果指针可以指向程序中的数据,那么使指针指向程序代码是否可能?
答:是可能的。 - 声明*int p = &i; * 和语句 p = &i不一致。为什么在语句中p没有像其在声明中那样前面加 号呢?
答:造成困惑的根源在于,根据上下文的不同,C语言中的 号可以有多种含义。在声明 int p = &i;中 号 不是间接寻址运算符,起作用是指明p的类型以便告知编译器p是一个指向int类型变量指针,而在语句中出现时, 号会执行间接寻址。语句 * p = &i;是不正确的,因为它把i的地址赋给了p指向的对象,而不是p本身。 - 有没有办法显示变量的地址?
答:任何指针(包括变量的地址)都可以通过调用printf函数并在格式串中使用转换说明%p来显示 。
5.下列声明是什么意思 void f (const int * p),不是说函数f不能够修改吗?
void f(const int *p);
{
int j;
*p = 0; //wrong
p = &j;
}
答:因为实际参数是按照值传递的,所以通过使指针指向其他地方的方法给p赋新值不会对函数外部产生任何影响。
0 条评论