← 返回首页
C语言基础(四)
发表时间:2024-05-21 02:31:16
指针/指向字符串的指针/指向数组的指针/指向函数的指针/指向指针的指针

指针

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

正如您所知道的,每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址。

请看下面的实例,它将输出定义的变量地址:

#include <stdio.h>

int main ()
{
    int var_runoob = 10;
    int *p;              // 定义指针变量
    p = &var_runoob;

   printf("var_runoob 变量的地址: %p\n", p);
   return 0;
}

img

通过上面的实例,我们了解了什么是内存地址以及如何访问它。接下来让我们看看什么是指针。

1.什么是指针

指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

type *var_name;

在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:

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

所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。

不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

2.如何使用指针

使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。下面的实例涉及到了这些操作:

#include <stdio.h>

int main ()
{
   int  var = 20;   /* 实际变量的声明 */
   int  *ip;        /* 指针变量的声明 */

   ip = &var;  /* 在指针变量中存储 var 的地址 */

   printf("var 变量的地址: %p\n", &var  );

   /* 在指针变量中存储的地址 */
   printf("ip 变量存储的地址: %p\n", ip );

   /* 使用指针访问值 */
   printf("*ip 变量的值: %d\n", *ip );
   return 0;
}

3.指针运算

指针是一个用数值表示的地址。因此,您可以对指针执行算术运算。可以对指针进行四种算术运算:++、--、+、-。

假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,让我们对该指针执行下列的算术运算:

ptr++

在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。

我们概括一下:

1.递增一个指针

我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。下面的程序递增变量指针,以便顺序访问数组中的每一个元素:

#include <stdio.h>

const int MAX = 3;

int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;

   /* 指针中的数组地址 */
   ptr = var;
   for ( i = 0; i < MAX; i++)
   {

      printf("存储地址:var[%d] = %p\n", i, ptr );
      printf("存储值:var[%d] = %d\n", i, *ptr );

      /* 指向下一个位置 */
      ptr++;
   }
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

存储地址:var[0] = e4a298cc
存储值:var[0] = 10
存储地址:var[1] = e4a298d0
存储值:var[1] = 100
存储地址:var[2] = e4a298d4
存储值:var[2] = 200

2.递减一个指针

同样地,对指针进行递减运算,即把值减去其数据类型的字节数,如下所示

#include <stdio.h>

const int MAX = 3;

int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;

   /* 指针中最后一个元素的地址 */
   ptr = &var[MAX-1];
   for ( i = MAX; i > 0; i--)
   {

      printf("存储地址:var[%d] = %p\n", i-1, ptr );
      printf("存储值:var[%d] = %d\n", i-1, *ptr );

      /* 指向下一个位置 */
      ptr--;
   }
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

存储地址:var[2] = 518a0ae4
存储值:var[2] = 200
存储地址:var[1] = 518a0ae0
存储值:var[1] = 100
存储地址:var[0] = 518a0adc
存储值:var[0] = 10

4.指向字符串的指针

在C语言中,一个指针通常指向一个变量的地址,但是它也可以指向一个字符串。一个指针,指向一个字符串,其实就是一个指向字符串第一个字符的指针。

1.用指针遍历字符串的所有字符

#include <string.h>
#include <stdio.h>
int main()
{
    char *p;// 定义一个指针p

    // 定义一个数组s存放字符串
    char s[] = "helloworld";

    // 指针p指向字符串的首字符'm'
    p = s; // 或者 p = &s[0];

    for (; *p != '\0'; p++) {
        printf("%c \n", *p);
    }
}

2.指针指向字符串

从前面可以看出,指针确实可以指向字符串并操作字符串。不过前面的做法是:先定义一个字符串数组存放字符串,然后将数组首地址传给指针p,让p指向字符串的首字符。

1.我们也可以直接用指针指向一个字符串,省略定义字符数组这个步骤

#include <string.h>
#include <stdio.h>
int main()
{
    // 定义一个字符串,用指针s指向这个字符串
    char *s = "helloworld";

    // 使用strlen函数测量字符串长度
    int len = strlen(s);

    printf("字符串长度:%d", len);
    return 0;
}

3.字符串相关处理函数

size_t   strlen(const char *);
char    *strcpy(char *, const char *); // 字符串拷贝函数
char    *strcat(char *, const char *); // 字符串拼接函数
int     strcmp(const char *, const char *); // 字符串比较函数

它们的参数都是指向字符变量的指针类型,因此可以传入指针变量或者数组名。

4.指针处理字符串的注意事项

字符指针与字符数组的区别:

1.方案一
// 定义一个字符串变量"lmj"
1.char a[] = "helloworld";
2.
3.// 将字符串的首字符改为'L'
4.*a = 'H';
5.printf("%s", a);

运行结果

Helloworld
2.方案二
1.char *p2 = "helloworld";
2.*p2 = 'H';
3.
4.printf("%s", p2);

看起来似乎是可行的,但这是错误代码,错在第2行。首先看第1行,指针变量p2指向的是一块字符串常量,正因为是常量,所以它内部的字符是不允许修改的。

3.综合实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char arr[]="https://www.baidu.com";
    char* str1="https://www.baidu.com";//str1是字符串常量。 
    arr[0]='H';
    //str1[0]='H'; //错误,因为str1是字符串常量,只能读取不能写入。 
    puts(arr);

    puts(str1);
    //arr="hello,world"; //只能在定义的同时初始化。 
    str1="hello,world!"; //str1指向另一个字符串常量。 
    //str1[0]='H'; //错误。 
    puts(str1);

    str1=arr;
    str1[0]='H';//ok,因为此时str1指向的是字符串变量。 
    puts(str1);
    return 0;
}

执行结果:

Https://www.baidu.com
https://www.baidu.com
hello,world!
Https://www.baidu.com

5.指针和数组

1.数组名

在 C 语言中,数组名表示数组的地址,即数组首元素的地址。当我们在声明和定义一个数组时,该数组名就代表着该数组的地址。

例如,在以下代码中:

int myArray[5] = {10, 20, 30, 40, 50};

在这里,myArray 是数组名,它表示整数类型的数组,包含 5 个元素。myArray 也代表着数组的地址,即第一个元素的地址。

数组名本身是一个常量指针,意味着它的值是不能被改变的,一旦确定,就不能再指向其他地方。

我们可以使用&运算符来获取数组的地址,如下所示:

int myArray[5] = {10, 20, 30, 40, 50};
int *ptr = &myArray[0]; // 或者直接写作 int *ptr = myArray;

在上面的例子中,ptr 指针变量被初始化为 myArray 的地址,即数组的第一个元素的地址。

需要注意的是,虽然数组名表示数组的地址,但在大多数情况下,数组名会自动转换为指向数组首元素的指针。这意味着我们可以直接将数组名用于指针运算,例如在函数传递参数或遍历数组时:

void printArray(int arr[], int size) {
  for (int i = 0; i < size; i++) {
    printf("%d ", arr[i]); *// 数组名arr被当作指针使用*
  }
}

int main() {
  int myArray[5] = {10, 20, 30, 40, 50};
  printArray(myArray, 5); *// 将数组名传递给函数*
  return 0;
}

在上述代码中,printArray 函数接受一个整数数组和数组大小作为参数,我们将 myArray 数组名传递给函数,函数内部可以像使用指针一样使用 arr 数组名。

2.指向数组的之中的指针

组名本身是一个常量指针,意味着它的值是不能被改变的,一旦确定,就不能再指向其他地方。

因此,在下面的声明中:

double balance[50];

balance 是一个指向 &balance[0] 的指针,即数组 balance 的第一个元素的地址。因此,下面的程序片段把 p 赋值为 balance 的第一个元素的地址:

double *p;
double balance[10];

p = balance;

使用数组名作为常量指针是合法的,反之亦然。因此,*(balance + 4) 是一种访问 balance[4] 数据的合法方式。

一旦您把第一个元素的地址存储在 p 中,您就可以使用 p、(p+1)、*(p+2) 等来访问数组元素。下面的实例演示了上面讨论到的这些概念:

#include <stdio.h>

int main ()
{
   /* 带有 5 个元素的整型数组 */
   double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0};
   double *p;
   int i;

   p = balance;

   /* 输出数组中每个元素的值 */
   printf( "使用指针的数组值\n");
   for ( i = 0; i < 5; i++ )
   {
       printf("*(p + %d) : %f\n",  i, *(p + i) );
   }

   printf( "使用 balance 作为地址的数组值\n");
   for ( i = 0; i < 5; i++ )
   {
       printf("*(balance + %d) : %f\n",  i, *(balance + i) );
   }

   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

使用指针的数组值
*(p + 0) : 1000.000000
*(p + 1) : 2.000000
*(p + 2) : 3.400000
*(p + 3) : 17.000000
*(p + 4) : 50.000000
使用 balance 作为地址的数组值
*(balance + 0) : 1000.000000
*(balance + 1) : 2.000000
*(balance + 2) : 3.400000
*(balance + 3) : 17.000000
*(balance + 4) : 50.000000

3.深入理解数组名

int arr[] = {1,2,3,4,5};

注意:arr本身不是一个指针变量,既然不是变量也就不需要另外存储空间,所以arr本身用&取地址是没有意义的,很多教材说数组名就是一个指向数组的一个元素地址的指针常量。这里要特别强调下,是指针常量,而不是常量指针,所以其本质就是个常量,而不是变量。其定义形式如下:

int * const pt;

通过这个定义也可以说明arr本身不是变量,arr本身不是地址而是指代整个数组,只不过会隐式转成指针罢了,array代表的是数组元素array[0]的地址。而&array代表的是整个一维数组的地址,这样就巧了出现arr,&arr和&arr[0]的值都相同。

但是arr与&arr表示的含义完全不同,&arr代表的是整个一维数组的地址,&arr用来计算下一个同样长度的数组开始的位置,它应从arr[0]开始,而arr代表的是数组第一个元素的地址。

看下面实例:

#include <stdio.h>

int main(int argc,char *argv[])
{

   int arr[] = {1,2,3,4,5,6,7,8,9,10};

   int *p = arr;

   printf("%d\n",p);
   printf("%d\n",*p);
   printf("%d\n",&p);
   printf("%d\n",&arr);  //注意:本身对arr取地址是没有意义的,arr === &arr
   printf("%d\n",arr); 
   printf("%d\n",&arr[0]); 

   printf("\n------------------------\n");
   printf("arr is %x, &arr is %x\n", arr, &arr);
   printf("arr + 1 is %x, &arr +1 is %x\n", arr + 1, &arr + 1); //&arr+1 比&arr 大40.

   return 0;    
}

运行结果:

6487536
1
6487528
6487536
6487536
6487536

------------------------
arr is 62fdf0, &arr is 62fdf0
arr + 1 is 62fdf4, &arr +1 is 62fe18

4.指针数组

C 指针数组是一个数组,其中的每个元素都是指向某种数据类型的指针。

指针数组存储了一组指针,每个指针可以指向不同的数据对象。

指针数组通常用于处理多个数据对象,例如字符串数组或其他复杂数据结构的数组。

可能有一种情况,我们想要让数组存储指向 int 或 char 或其他数据类型的指针。

下面是一个指向整数的指针数组的声明:

int *ptr[MAX];

在这里,把 ptr 声明为一个数组,由 MAX 个整数指针组成。因此,ptr 中的每个元素,都是一个指向 int 值的指针。下面的实例用到了三个整数,它们将存储在一个指针数组中,如下所示:

#include <stdio.h>
const int MAX = 3;
int main ()
{
   int  var[] = {10, 100, 200};
   int i, *ptr[MAX];

   for ( i = 0; i < MAX; i++)
   {
      ptr[i] = &var[i]; /* 赋值为整数的地址 */
   }
   for ( i = 0; i < MAX; i++)
   {
      printf("Value of var[%d] = %d\n", i, *ptr[i] );
   }
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Value of var[0] = 10
Value of var[1] = 100
Value of var[2] = 200

您也可以用一个指向字符的指针数组来存储一个字符串列表,如下:

#include <stdio.h>
const int MAX = 4;
int main ()
{
   const char *names[] = {
                   "Zara Ali",
                   "Hina Ali",
                   "Nuha Ali",
                   "Sara Ali",
   };
   int i = 0;

   for ( i = 0; i < MAX; i++)
   {
      printf("Value of names[%d] = %s\n", i, names[i] );
   }
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali

6.函数指针

函数指针是指向函数的指针变量。

通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。

函数指针可以像一般函数一样,用于调用函数、传递参数。

函数指针类型的声明:

typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型

以下实例声明了函数指针变量 p,指向函数 max:

#include <stdio.h>

int max(int x, int y)
{
    return x > y ? x : y;
}

int main(void)
{
    /* p 是函数指针 */
    int (* p)(int, int) = & max; // &可以省略
    int a, b, c, d;

    printf("请输入三个数字:");
    scanf("%d %d %d", & a, & b, & c);

    /* 与直接调用函数等价,d = max(max(a, b), c) */
    d = p(p(a, b), c); 

    printf("最大的数字是: %d\n", d);

    return 0;
}

编译执行,输出结果如下:

请输入三个数字:1 2 3
最大的数字是: 3

7.回调函数---函数指针作为某个函数的参数

函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。

简单讲:回调函数是由别人的函数执行时调用你实现的函数。

实例中 populate_array() 函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。

实例中我们定义了回调函数 getNextRandomValue(),它返回一个随机值,它作为一个函数指针传递给 populate_array() 函数。

populate_array() 将调用 10 次回调函数,并将回调函数的返回值赋值给数组。

#include <stdlib.h>  
#include <stdio.h>

void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
    for (size_t i=0; i<arraySize; i++)
        array[i] = getNextValue();
}

// 获取随机值
int getNextRandomValue(void)
{
    return rand();
}

int main(void)
{
    int myarray[10];
    /* getNextRandomValue 不能加括号,否则无法编译,因为加上括号之后相当于传入此参数时传入了 int , 而不是函数指针*/
    populate_array(myarray, 10, getNextRandomValue);
    for(int i = 0; i < 10; i++) {
        printf("%d ", myarray[i]);
    }
    printf("\n");
    return 0;
}

8.指向指针的指针

如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。

例如:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main() {
    int x = 100;
    int *p = &x;
    int** pt=&p;

    printf("pt指向的指针所指向的内存单元的值是:%d\n",**pt);
    printf("p指向的内存单元的值是:%d\n",*p);
    printf("x的值是:%d\n",x);

    printf("pt的值是:%d\n",pt);
    printf("p的地址是:%d\n",&p);

    return 0;
}

运行结果:

pt指向的指针所指向的内存单元的值是:100
p指向的内存单元的值是:100
x的值是:100
pt的值是:6487560
p的地址是:6487560

x,p和pt关系图如下:

同理我们如果要定义一个三级指针指向pt,可以这样写:

int*** ppt =&pt;

但是在实际开发中最多会用到二级指针,几乎用不到二级以上的高级指针。