chap08:指针

发布时间:2024年01月02日


通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。也有一种说法,C语言的精华就在指针。

所谓指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。不过,人们往往不会区分两者的概念,而是混淆在一起使用,在必要的情况下,大家也要注意区分。

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。

下图是 4G 内存中每个字节的编号(以十六进制表示):

在这里插入图片描述

我们将内存中字节的编号称为地址(Address)或指针(Pointer)。

#include <stdio.h>
int main(){
    int a = 100;
    char str[20] = "Hello world.";
    printf("%#X, %#X\n",&a,str);
    //控制符%p,专门用来以十六进制形式输出地址,不过%p的输出格式并不统一
    printf("%p, %p\n",&a,str);
	return 0; 
}

虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。

一、指针变量的定义和使用

1、指针变量的定义

指针变量声明的一般形式为:

type *var_name;

*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型 。例如:

int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;    /* 一个字符型的指针 */

在这里插入图片描述

2、指针变量的使用

指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:

*pointer

这里的*称为指针运算符,用来取得某个地址上的数据

#include <stdio.h>
int main(){
    int a = 15;
    int *p = &a;
    printf("%d, %d\n", a, *p);  // 两种方式都可以输出a的值
    // *p等价于a,也可以修改内存上的数据
    *p = 20;
    printf("%d, %d\n", a, *p);
    return 0;
}

使用指针是间接访问数据,使用变量名是直接访问数据,前者比后者的代价要高。

3、指针变量的运算(加法、减法和比较运算)

指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等(不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义)。

加法、减法:可以理解为移动指针。比较运算是比较指针的前后关系。

请看下面的代码:

#include <stdio.h>
int main(){
    int    a = 10,   *pa = &a, *paa = &a;
    double b = 99.9, *pb = &b;
    char   c = '@',  *pc = &c;
    //最初的值
    printf("&a=%d, &b=%d, &c=%d\n", &a, &b, &c);
    printf("pa=%d, pb=%d, pc=%d\n", pa, pb, pc);
    //加法运算
    printf("指针加1,注意不同类型指针地址值的变化\n");
    pa++; pb++; pc++;
    printf("pa=%d, pb=%d, pc=%d\n", pa, pb, pc);
    //减法运算
    printf("指针减1,注意地址又回到原先的值\n");
    pa--; pb--; pc--;
    printf("pa=%d, pb=%d, pc=%d\n", pa, pb, pc);
    //比较运算
    printf("两个指针相比较\n");
    if(pa == paa){
        printf("pa与paa指向同一个地方\n");
    }else{
        printf("pa与paa指向不同一个地方\n");
    }
    return 0;
}

注意: 不同类型指针,移动的距离不一样!

对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为不知道它变化后指向的是什么数据。这里只是为了理解不同类型指针加减移动的距离不同。

二、数组指针(指向数组的指针)

  • 数组中的所有元素在内存中是连续排列的,正好指针的加减运算可以为访问数组元素提供另外的方式。
  • 数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。是一个常量

遍历数组元素:

#include <stdio.h>
int main(){
    int arr[5] = { 99, 15, 100, 888, 252 };
    int *p = arr;
    int i;
    printf("第一种访问方式:\n");
    for(i=0; i<5; i++){
        printf("%d  ", arr[i]);  
    }
    printf("\n");
    printf("第二种访问方式:\n");
    for(i=0; i<5; i++){
        printf("%d  ", p[i]);  
    }
    printf("\n"); 
    printf("第三种访问方式:\n");
    for(i=0; i<5; i++){
        printf("%d  ", *(arr+i));  
    }
    printf("\n");
    printf("第四种访问方式:\n");
    for(i=0; i<5; i++){
        printf("%d  ", *(p+i));  
    }
    printf("\n");
	printf("第五种访问方式:\n");
    for(p=arr; p<arr+5; p++){
    	// 注意循环结束指针p所指的位置 
        printf("%d  ", *p);  
    }		   
    return 0;
}

三、字符串指针

前面学过,C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中。

字符数组归根结底还是一个数组,上节讲到的关于指针和数组的规则同样也适用于字符数组。

#include <stdio.h>
#include <string.h>
int main(){
    char str[] = "Hello world.";
    char *pstr = str;
    
    printf("字符数组的长度为:%d\n",sizeof(str));
    printf("字符串的长度为:%d\n",strlen(str));
    puts(str);
	puts(pstr);
	   
    return 0;
}

除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:

#include <stdio.h>
#include <string.h>
int main(){   
    char *pstr = "Hello world.";
    
    printf("字符数组的长度为:%d\n",sizeof(pstr));
    printf("字符串的长度为:%d\n",strlen(pstr));
	puts(pstr);
	   
    return 0;
}

它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。

四、指针变量作为函数参数

在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。

比较下面两个程序,理解(值传递与地址传递的不同):

#include <stdio.h>
// 交换两个变量的值 
void swap(int a, int b){  // 传递的是值
    int temp;  //临时变量
    temp = a;
    a = b;
    b = temp;
}
int main(){
    int a = 66, b = 99;
    swap(a, b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}
#include <stdio.h>
void swap(int *p1, int *p2){
    int temp;  //临时变量
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
int main(){
    int a = 66, b = 99;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

五、用数组作函数参数

#include <stdio.h>
int max(int intArr[6], int n){
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<n; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
    return maxValue;
}

int main(){
    int nums[6], i;
    int len = sizeof(nums)/sizeof(int);
    //读取用户输入的数据并赋值给数组元素
    for(i=0; i<len; i++){
        scanf("%d", nums+i);
    }
    printf("Max value is %d!\n", max(nums, len));
    return 0;
}

用数组做函数参数时,实际上传递的是数组的首地址,仅仅是一个指针,而不是真正的数组。

所以:int max(int intArr[6], int n)中的数组长度写了也是无意义的,等价于:

int max(int intArr[], int n)

等价于:

int max(int *intArr, int n){
    int i, maxValue = intArr[0];  //假设第0个元素是最大值
    for(i=1; i<n; i++){
        if(maxValue < intArr[i]){
            maxValue = intArr[i];
        }
    }
   
    return maxValue;
}

在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。

六、二维数组指针(指向二维数组的指针)

C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]a[0][1]a[0][2]a[0][3]

假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示:
在这里插入图片描述

我们先来定义一个指向 a 的指针变量 p:

int (*p)[4] = a;

括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。

[ ]的优先级高于*( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针。

对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。

数组名 a 在表达式中也会被转换为和 p 等价的指针!

下面我们就来探索一下如何使用指针 p 来访问二维数组中的每个元素。按照上面的定义:

  1. p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行。

  2. *(p+1)表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个元素,下面的运行结果有力地证明了这一点:

#include <stdio.h>
int main(){
    int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
    int (*p)[4] = a;
    printf("%d\n", sizeof(*(p+1)));
    return 0;
}

运行结果:
16

  1. *(p+1)+1表示第 1 行第 1 个元素的地址。如何理解呢?

*(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。

  1. *(*(p+1)+1)表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。

根据上面的结论,可以很容易推出以下的等价关系:

a+i == p+i  // 行地址
a[i] == p[i] == *(a+i) == *(p+i)  // 列元素地址
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)  // 数组元素

【实例】使用指针遍历二维数组。

#include <stdio.h>
int main(){
    int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
    int(*p)[4];
    int i,j;
    p=a;
    for(i=0; i<3; i++){
        for(j=0; j<4; j++) printf("%2d  ",*(*(p+i)+j)); // 思考一下有几种方式
        printf("\n");
    }
    return 0;
}
文章来源:https://blog.csdn.net/qq_22891627/article/details/135319358
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。