当前位置: 首页 > news >正文

C语言基础之【指针】(中)

C语言基础之【指针】(中)

  • 指针和数组
    • 数组名
    • 指针操作数组元素
    • 数组名加减运算
    • 指针加减运算
      • 加法运算
      • 减法运算
      • 指针数组
  • 多级指针
  • 指针和函数
    • 函数形参改变实参的值
    • 数组名做函数参数
    • 指针作为函数的参数
    • 指针作为函数的返回值

往期《C语言基础系列》回顾:
链接:
C语言基础之【C语言概述】
C语言基础之【数据类型】(上)
C语言基础之【数据类型】(下)
C语言基础之【运算符与表达式】
C语言基础之【程序流程结构】
C语言基础之【数组和字符串】(上)
C语言基础之【数组和字符串】(下)
C语言基础之【函数】
C语言基础之【指针】(上)


指针和数组

数组名

数组名是一个指向数组首元素的指针常量 ---> arr == &arr[0]

  • 数组名的值不能被修改(即:不能指向其他地址)

    int arr[3] = {10, 20, 30};
                        
    int *p = arr; // arr是一个指向arr[0]的指针,所以我们不必&arr这样来获取数组的地址
    arr = 10; //err, 数组名只是常量,不能修改
    

数组和指针的关系:

  1. 数组名与指针变量

    • 数组名可以赋值给指针变量

      int arr[3] = {10, 20, 30};
      int *p = arr; // p指向arr[0]
      
  2. 数组下标与指针算术

    • 数组下标操作 arr[i] 等价于指针算术 *(arr + i)

      int arr[3] = {10, 20, 30};
                                                        
      printf("%d\n", arr[1]);    // 输出20
      printf("%d\n", *(arr + 1)); // 输出20
      

数组和指针的区别:

特性数组指针
定义存储一组相同类型的元素存储一个内存地址
内存分配静态分配,大小固定动态分配,大小可变
大小sizeof(arr) 返回整个数组的大小sizeof(p) 返回指针的大小(通常为4或8字节)
赋值数组名是常量,不能赋值指针是变量,可以赋值
参数传递数组名退化为指针直接传递指针

指针操作数组元素

指针操作数组元素:是利用指针的算术运算解引用操作来访问和修改数组中的数据。


通过指针访问数组元素:

1.使用指针算术

  • 指针算术允许通过加减操作移动指针的位置。
    • 例如:p + i 表示指向 arr[i] 的指针
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;

printf("%d\n", *(p + 0)); // 输出arr[0]:10
printf("%d\n", *(p + 1)); // 输出arr[1]:20
printf("%d\n", *(p + 2)); // 输出arr[2]:30

2.使用数组下标

  • 指针变量可以像数组一样使用下标访问元素。
    • 例如:p[i] 等价于 *(p + i)
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;

printf("%d\n", p[0]); // 输出arr[0]:10
printf("%d\n", p[1]); // 输出arr[1]:20
printf("%d\n", p[2]); // 输出arr[2]:30

通过指针遍历数组元素:

1.通过指针变量指向数组

可以定义一个指针变量,使其指向数组的首地址,然后通过指针来访问和操作数组元素。

  • 指针p指向数组arr的首元素
  • 通过*(p + i)的方式可以访问和修改数组中的第i个元素
#include <stdio.h>

int main()
{
    int arr[] = { 1, 2, 3, 4, 5 };
    int* p = arr;  // 指针p指向数组arr的首元素

    // 通过指针访问数组元素
    for (int i = 0; i < 5; i++)
    {
        printf("%d ", *(p + i));
    }

    // 通过指针修改数组元素
    *(p + 2) = 10;
    printf("\n");

    // 再次输出数组元素,验证修改是否成功
    for (int i = 0; i < 5; i++)
    {
        printf("%d ", *(p + i));
    }

    return 0;
}

输出:

1 2 3 4 5
1 2 10 4 5


2.利用数组名作为指针

数组名本身就可以看作是一个指向数组首元素的常量指针,因此可以直接利用数组名来操作数组元素。

  • arr就相当于一个指向arr[0]的指针
  • *(arr + i)等价于arr[i],可以方便地进行数组元素的访问和修改
#include <stdio.h>

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

    // 通过数组名访问数组元素
    for (int i = 0; i < 5; i++)
    {
        printf("%d ", *(arr + i));
    }

    // 通过数组名修改数组元素
    *(arr + 3) = 20;
    printf("\n");

    // 再次输出数组元素,验证修改是否成功
    for (int i = 0; i < 5; i++)
    {
        printf("%d ", *(arr + i));
    }

    return 0;
}

输出:

1 2 3 4 5
1 2 3 20 5


3.使用指针的自增操作

可以通过对指针进行自增操作,使其依次指向数组中的每个元素,从而实现对数组元素的操作。

  • 通过p++使指针p依次指向数组的每个元素,从而实现了对数组元素的访问和修改。
#include <stdio.h>

int main()
{
    int arr[] = { 1, 2, 3, 4, 5 };
    int* p = arr;

    // 通过指针自增访问数组元素
    while (p < arr + 5)
    {
        printf("%d ", *p);
        p++;
    }//打印结束后,p指向一块无效地址空间 (p变成野指针)

    // 将指针重新指向数组首元素
    p = arr;

    // 通过指针自增修改数组元素
    while (p < arr + 5)
    {
        *p *= 2;
        p++;
    }
    printf("\n");

    // 再次输出数组元素,验证修改是否成功
    p = arr;
    while (p < arr + 5)
    {
        printf("%d ", *p);
        p++;
    }

    return 0;
}

输出:

1 2 3 4 5
2 4 6 8 10

综上所述arr[0] == *(arr + 0) == p[0] == *(p + 0)

数组名加减运算

int main(void)
{
    short a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    
    printf("a = %p\n", a);
    printf("&a[0] = %p\n", &a[0]);
    printf("a+1 = %p\n", a+1);
    printf("&a  = %p\n", &a);
    printf("&a+1 = %p\n", &a + 1);
    
    system("pause");
    return EXIT_SUCCESS;
}

分析

  1. printf("a = %p\n", a);:输出的是数组 a 第一个元素的地址。
    • 在 C 语言中,数组名代表数组首元素的地址,所以这里输出的是数组 a 第一个元素的地址。
  2. printf("&a[0] = %p\n", &a[0]);:输出的是数组 a 第一个元素的地址。
  3. printf("a+1 = %p\n", a+1);:输出的是数组 a 第二个元素的地址。
    • 由于 ashort 类型数组,a + 1 表示跳过一个 short 类型元素的地址,即指向数组第二个元素的地址。
  4. printf("&a = %p\n", &a);:输出的是整个数组的地址。
    • 输出数组 a 本身的地址,虽然值上可能和 a 相同,但意义不同,&a 代表整个数组的地址。
  5. printf("&a+1 = %p\n", &a + 1);:输出的是跳过整个数组的地址。
    • 因为 &a 代表整个数组的地址,&a + 1 则是跳过整个数组的地址。

输出

a = 00000010D7EFF6D8
&a[0] = 00000010D7EFF6D8
a+1 = 00000010D7EFF6DA
&a  = 00000010D7EFF6D8
&a+1 = 00000010D7EFF6EC

指针加减运算

指针的加减运算:通过移动指针的位置来访问和操作内存中的数据。

  • 指针加减运算的结果:是一个新的指针。
    • 指向原指针向前或向后移动若干单位后的内存地址
  • 指针加减运算的单位:是指针所指向数据类型的大小。
    • int *p 的加减单位是 sizeof(int)(通常为4字节)
    • char *p 的加减单位是 sizeof(char)(通常为1字节)

注意:指针与整数的区别

  • 指针存储的是内存地址,而整数存储的是数值。
  • 指针运算考虑了数据类型的大小,而整数运算直接操作数值。

加法运算

指向基本数据类型的指针的加法:

底层原理新地址 = 原地址 + (n * sizeof(数据类型))

  • 例如:int *p + 2 的实际计算:

    新地址 = p + (2 * sizeof(int))  
    

指向数组的指针的加法:

  • 指针加法的语法指针 + 整数

  • 结果是:指针向后移动 n 个单位(n 是整数)

    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr; // p指向arr[0]
                       
    p = p + 2; // p指向arr[2]
    printf("%d\n", *p); // 输出30
    

指针与指针的加法:

注意在 C 语言中,直接将两个指针相加是没有实际意义的

  • 因为这样的操作不符合逻辑,也无法得到有价值的结果,所以通常不允许进行指针与指针的加法运算。

减法运算

指向基本数据类型的指针的减法:

底层原理新地址 = 原地址 - (n * sizeof(数据类型))

  • 例如:char *p - 2 的实际计算:

    新地址 = p - (2 * sizeof(char))
    

指向数组的指针的减法:

  • 指针减法的语法指针 - 整数

  • 结果是:指针向前移动 n 个单位(n 是整数)

    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr + 4; // p指向arr[4]
                        
    p = p - 2; // p指向arr[2]
    printf("%d\n", *p); // 输出30
    

指针与指针的减法:

指针与指针的减法语法指针1 - 指针2

  • 两个指针指向同一数组时:

    • 结果是:两个指针之间的元素个数(而不是字节数)
  • 两个指针指向非同一数组时:

    • 结果是:行为未定义

      int arr[5] = {10, 20, 30, 40, 50};
      int *p1 = arr + 2; // p1指向arr[2]
      int *p2 = arr;     // p2指向arr[0]
                                              
      printf("%ld\n", p1 - p2); // 输出2(p1和p2之间相差2个元素)
      

示例代码:指针加减运算的使用

#include <stdio.h>

int main()
{
	int arr[5] = { 10, 20, 30, 40, 50 };
	int* p = arr; // p指向arr[0]

	// 指针加法
	p = p + 2; // p指向arr[2]
	printf("p + 2: %d\n", *p); // 输出30

	// 指针减法
	p = p - 1; // p指向arr[1]
	printf("p - 1: %d\n", *p); // 输出20

	// 指针与指针的减法
	int* p1 = arr + 3; // p1指向arr[3]
	int* p2 = arr;     // p2指向arr[0]
	printf("p1 - p2: %ld\n", p1 - p2); // 输出3

	// 遍历数组
	for (int* ptr = arr; ptr < arr + 5; ptr++)
	{
		printf("%d ", *ptr); // 输出:10 20 30 40 50
	}

	return 0;
}

输出:

p + 2: 30
p - 1: 20
p1 - p2: 3
10 20 30 40 50

示例代码:探究指针与指针的减法

#include <stdio.h>

int main()
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int* p1 = &a[1]; // p1指向a[1]的地址
	int* p2 = &a[2]; // p2指向a[2]的地址
	printf("p1 = %p, p2 = %p\n", p1, p2);

	int n1 = p2 - p1; // 指针减法,计算p2和p1之间的元素个数
	int n2 = (int)p2 - (int)p1; // 将指针转换为整数,计算地址的字节差
	printf("n1 = %d, n2 = %d\n", n1, n2);

	return 0;
}

输出:

p1 = 0x1004, p2 = 0x1008
n1 = 1, n2 = 4

分析 p1,p2:

  1. 定义数组 a

    • a 是一个包含 9 个整数的数组,初始化为 {1, 2, 3, 4, 5, 6, 7, 8, 9}
    • 假设数组 a 的起始地址为 0x1000,则:
      • a[0] 的地址为 0x1000
      • a[1] 的地址为 0x1004(假设 int 占 4 字节)
      • a[2] 的地址为 0x1008
  2. 定义指针 p1p2

    • p1 指向 a[1] 的地址,即 0x1004
    • p2 指向 a[2] 的地址,即 0x1008
  3. 输出 p1p2 的值

    • printf("p1 = %p, p2 = %p\n", p1, p2);

      p1 = 0x1004, p2 = 0x1008
      

分析 n1,n2:

  1. 计算 n1n2

    • n1 = p2 - p1; 这是指针减法,计算的是两个指针之间的元素个数
      • p2p1 分别指向 a[2]a[1],它们之间相差 1 个元素。
    • n2 = (int)p2 - (int)p1; 这是将指针转换为整数后计算地址的字节差
      • p2 的地址是 0x1008p1 的地址是 0x1004
      • 0x1008 - 0x1004 = 4(假设 int 占 4 字节)
  2. 输出 n1n2 的值

    • printf("n1 = %d, n2 = %d\n", n1, n2);

      n1 = 1, n2 = 4
      

关键点总结:

  1. 指针减法 p2 - p1

    • 计算的是两个指针之间的元素个数,而不是字节数。
    • 结果是一个整数,表示两个指针之间相差多少个元素。
  2. 指针转换为整数 (int)p2 - (int)p1

    • 将指针转换为整数后,计算的是两个地址之间的字节差。
    • 结果是一个整数,表示两个地址之间相差多少字节。

根据上面的知识学习,我们可以总结出这样一个小技巧。

代码示例:利用指针计算数组中的元素个数

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

int main(void)
{
    int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    int* p = a;

    for (size_t i = 0; i < 10; i++)
    {
        p++;
    }
    printf("p - a = %d\n", p - a);

    system("pause");
    return EXIT_SUCCESS;
}

代码示例:重更新实现strlen() 函数

// 借助数组实现
int mystrlen(char str[]) 
{
    int i = 0;
    while (str[i] != '\0')
    {
        i++;
    }
    return i;
}

// 借助指针++实现
int mystrlen2(char str[]) 
{
    char* p = str;
    while (*p != '\0')
    {
        p++;
    }
    return p - str; // 返回数组元素的个数
}

指针数组

指针数组:是一个数组,其元素都是指针。

  • 指针数组中的每个指针可以指向不同类型的数据(如: intcharfloat 等)
  • 指针数组中的每个指针也可以指向其他数组动态分配的内存

指针数组定义的语法数据类型 *数组名[数组大小];

  • 数据类型:指针所指向的数据类型。
  • 数组名:指针数组的名称。
  • 数组大小:数组中指针的数量。
int *arr[5];  //定义一个包含5个int指针的数组
char *strArr[] = {"Apple", "Banana", "Cherry"};  //定义一个包含3个字符串的数组
  • char *strArr[] = {"Apple", "Banana", "Cherry"};指针数组也被称为字符串数组

指针数组的初始化:

  1. 指向普通变量

    • 指针数组的元素可以指向普通变量。

      int a = 10, b = 20, c = 30;
      int *arr[3] = {&a, &b, &c}; // 初始化指针数组
      
  2. 指向字符串

    • 指针数组常用于存储字符串(字符串是字符数组)

      const char *strArr[] = {"Hello", "World", "C Language"}; // 初始化字符串指针数组
      
  3. 指向动态分配的内存

    • 指针数组的元素可以指向动态分配的内存。

      int *arr[3];
      for (int i = 0; i < 3; i++) 
      {
          arr[i] = (int *)malloc(sizeof(int)); // 动态分配内存
          *arr[i] = i + 1; // 初始化值
      }
      

代码示例:指针数组的本质

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

// 指针数组1
int main(void)
{
     int a = 10;
     int b = 20;
     int c = 30;

     int* p1 = &a;
     int* p2 = &b;
     int* p3 = &c;

     int* arr[] = { p1, p2, p3 };  // 整型指针数组arr, 存的都是整型地址

     printf("*(arr[0]) = %d\n", *(*(arr + 0))); //arr[0] ==  *(arr+0)
     printf("*(arr[0]) = %d\n", **arr);

     system("pause");
     return EXIT_SUCCESS;
}

输出

*(arr[0]) = 10
*(arr[0]) = 10

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

// 指针数组2
int main(void)
{
    int a[] = { 10 };
     int b[] = { 20 };
     int c[] = { 30 };
 
    int* arr[] = { a, b, c };  // 整型指针数组arr, 存的都是整型地址
 
    printf("*(arr[0]) = %d\n", *(*(arr + 0))); //arr[0] ==  *(arr+0)
     printf("*(arr[0]) = %d\n", **arr);
 
    system("pause");
     return EXIT_SUCCESS;
 }

输出

*(arr[0]) = 10
*(arr[0]) = 10

总结arr[0][0] == *(*(arr + 0) + 0) == **arr

指针数组本质,是一个二级指针;二维教组本质,也是一个二级指针

指针数组的使用:

1.访问指针数组的元素

  • 通过下标访问指针数组的元素,然后解引用指针以访问数据。

    int a = 10, b = 20, c = 30;
    int *arr[3] = {&a, &b, &c};
                                                                  
    for (int i = 0; i < 3; i++) 
    {
        printf("%d ", *arr[i]); // 输出:10 20 30
    }
    

2.遍历字符串指针数组

  • 字符串指针数组常用于存储多个字符串。

    char *strArr[] = {"Hello", "World", "C Language"};
    for (int i = 0; i < 3; i++) 
    {
        printf("%s\n", strArr[i]); // 输出每个字符串
    }
    

3.动态分配的多维数组

  • 指针数组可以用于实现动态分配的多维数组。

    #include <stdio.h>
    #include <stdlib.h>
                                                                  
    int main()
    {
        int* arr[3]; // 定义一个指针数组
        for (int i = 0; i < 3; i++)
        {
            arr[i] = (int*)malloc(4 * sizeof(int)); // 每个指针指向一个动态数组
            for (int j = 0; j < 4; j++)
            {
                arr[i][j] = i * 4 + j; // 初始化值
            }
        }
                                                                  
        // 输出动态分配的二维数组
        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                printf("%d ", arr[i][j]);
            }
            printf("\n");
        }
                                                                  
        // 释放内存
        for (int i = 0; i < 3; i++)
        {
            free(arr[i]);
        }
    	return 0;
    }
    

    输出:

    0 1 2 3
    4 5 6 7
    8 9 10 11

示例代码:指针数组的定义、初始化和使用

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

int main()
{
    // 示例1:指向普通变量的指针数组
    int a = 10, b = 20, c = 30;
    int* arr1[3] = { &a, &b, &c };
    for (int i = 0; i < 3; i++)
    {
        printf("%d ", *arr1[i]); // 输出:10 20 30
    }
    printf("\n");


    // 示例2:字符串指针数组
    char* arr2[] = { "Hello", "World", "C Language" };  //error:"const char *" 类型的值不能用于初始化 "char *" 类型的实体
    for (int i = 0; i < 3; i++)
    {
        printf("%s\n", arr2[i]); // 输出每个字符串
    }


    // 示例3:动态分配的多维数组
    int* arr3[3];
    for (int i = 0; i < 3; i++)
    {
        arr3[i] = (int*)malloc(4 * sizeof(int));
        for (int j = 0; j < 4; j++)
        {
            arr3[i][j] = i * 4 + j; // 初始化值
        }
    }
 // 输出动态分配的二维数组
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            printf("%d ", arr3[i][j]);
        }
        printf("\n");
    }
    // 释放内存
 for (int i = 0; i < 3; i++)
    {
        free(arr3[i]);
    }

    return 0;
}

代码错误片段:

char *arr2[] = {"Hello", "World", "C Language"}; 
//error:"const char *" 类型的值不能用于初始化 "char *" 类型的实体
//***真实的情况是C程序并不会报错,而是C++程序会报错。原因是C++对const的语法进行了加强,已不允许上面的行为了***//

错误原因:

  • "Hello""World""C Language" 这些字符串字面量是只读的(常量),因此它们的类型是 const char*
  • char* arr2[] 的类型是 char*

  • const char*:表示指向常量字符的指针。
  • char*:表示指向可修改字符的指针。

所以:将 const char* 赋值给 char* 会导致潜在的风险(可能 修改只读数据),因此编译器会报错。


解决方法:

1.使用 const char* 定义指针数组如果不需要修改字符串内容

  • 将 arr2 的类型改为 const char*,以匹配字符串字面量的类型。
 const char *arr2[] = {"Hello", "World", "C Language"};

这样,arr2 中的每个元素都是 const char* 类型,与字符串字面量的类型一致。


2.使用 char数组 存储字符串如果需要修改字符串内容

  • 将字符串字面量复制到可修改的char数组中。
 char arr2[3][20] = {"Hello", "World", "C Language"};

这样,arr2 是一个二维字符数组,每个字符串都存储在一个独立的 char数组 中,可以修改。


3.动态分配内存并复制字符串如果需要在运行时动态分配内存并存储字符串

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

int main()
{
   char* arr2[3];
   arr2[0] = (char*)malloc(20 * sizeof(char));
   arr2[1] = (char*)malloc(20 * sizeof(char));
   arr2[2] = (char*)malloc(20 * sizeof(char));

   strcpy(arr2[0], "Hello");
   strcpy(arr2[1], "World");
   strcpy(arr2[2], "C Language");

   for (int i = 0; i < 3; i++)
   {
       printf("%s\n", arr2[i]);
   }

   // 释放内存
   for (int i = 0; i < 3; i++)
   {
       free(arr2[i]);
   }

   return 0;
}

总结:

  • 如果不需要修改字符串内容,使用 const char* 定义指针数组。
  • 如果需要修改字符串内容,使用 char数组动态分配内存 定义指针数组。

多级指针

多级指针:是指向指针的指针。

多级指针通常用于动态内存管理多维数组函数参数传递等场景。


多级指针定义的语法

数据类型 *指针变量名;       // 一级指针:指向普通变量的指针
数据类型 **指针变量名;      // 二级指针:指向一级指针的指针
数据类型 ***指针变量名;     // 三级指针:指向二级指针的指针
int a = 10;

int *p = &a;      // p是一级指针,指向a
int **pp = &p;    // pp是二级指针,指向p
int ***ppp = &pp; // ppp是三级指针,指向pp

注意事项多级指针不能跳跃定义

***ppp == **pp == *p == a  //整型变量
**ppp == *pp == p == &a    //一级指针
*ppp == pp == &p;		   //二级指针
ppp == &pp;				   //三级指针

多级指针的使用:

1.访问变量的值

  • 通过多级指针可以间接访问变量的值。

    int a = 10;
    int *p = &a;      // p指向a
    int **pp = &p;    // pp指向p
    int ***ppp = &pp; // ppp指向pp
              
    printf("%d\n", ***ppp); // 输出10
    

2.动态内存分配

  • 多级指针常用于动态分配多维数组。

    #include <stdio.h>
    #include <stdlib.h>
              
    int main()
    {
    	// 分配行指针
    	int** arr = (int**)malloc(3 * sizeof(int*));
    	// 检查内存分配是否成功
    	if (arr == NULL)
    	{
    		printf("内存分配失败,无法分配行指针!\n");
    		return 1;
    	}
              
    	// 分配每行的列
    	for (int i = 0; i < 3; i++)
    	{
    		arr[i] = (int*)malloc(4 * sizeof(int));
    		// 检查内存分配是否成功
    		if (arr[i] == NULL)
    		{
    			printf("内存分配失败,无法分配第 %d 行的列!\n", i);
              
    			// 释放之前已经分配的内存
    			for (int j = 0; j < i; j++)
    			{
    				free(arr[j]);
    			}
    			free(arr);
    			return 1;
    		}
    	}
              
    	// 初始化数组
    	for (int i = 0; i < 3; i++)
    	{
    		for (int j = 0; j < 4; j++)
    		{
    			arr[i][j] = i * 4 + j;
    		}
    	}
              
    	// 打印数组元素
    	printf("初始化后的二维数组元素如下:\n");
    	for (int i = 0; i < 3; i++)
    	{
    		for (int j = 0; j < 4; j++)
    		{
    			printf("%d ", arr[i][j]);
    		}
    		printf("\n");
    	}
              
    	// 释放内存
    	for (int i = 0; i < 3; i++)
    	{
    		free(arr[i]);
    	}
    	free(arr);
              
    	return 0;
    }
    

    输出:

    初始化后的二维数组元素如下:
    0 1 2 3
    4 5 6 7
    8 9 10 11

3.函数参数传递

  • 多级指针可以用于在函数中修改指针的值。

    void allocateMemory(int **p) 
    {
        *p = (int *)malloc(sizeof(int)); // 修改一级指针的值
        **p = 100;                       // 修改指针指向的值
    }
              
    int main() 
    {
        int *p = NULL;
        allocateMemory(&p); // 传递一级指针的地址
        printf("%d\n", *p); // 输出100
                  
        free(p);
        return 0;
    }
    

指针和函数

函数形参改变实参的值

在 C 语言中,函数参数传递有两种方式:值传递地址传递

  • 函数参数传递默认是值传递,即函数内部对形参的修改不会影响实参的值。
  • 然而,通过使用指针,可以实现地址传递的效果,从而在函数内部修改实参的值。

值传递:是指将实参的值复制一份传递给函数的形参。

  • 在函数内部对形参的修改不会影响到实参的值。

    #include <stdio.h>
            
    void swap(int a, int b) 
    {
        int temp = a;
        a = b;
        b = temp;
    }
            
    int main() 
    {
        int x = 10, y = 20;
        swap(x, y);
        printf("x = %d, y = %d\n", x, y); // 输出:x = 10, y = 20
        return 0;
    }
    

地址传递:是指将实参的地址传递给函数的形参。

  • 在函数内部可以通过指针来修改实参的值。

    #include <stdio.h>
    
    void swap(int *a, int *b) 
    {
        int temp = *a;
        *a = *b;
        *b = temp;
    }
    
    int main() 
    {
        int x = 10, y = 20;
        swap(&x, &y); // 传递x和y的地址
        printf("x = %d, y = %d\n", x, y); // 输出:x = 20, y = 10
        return 0;
    }
    

示例代码:函数参数的两种传递方式

#include <stdio.h>

void swap1(int x, int y)
{
	int tmp;
	tmp = x;
	x = y;
	y = tmp;
	printf("x = %d, y = %d\n", x, y);
}

void swap2(int* x, int* y)
{
	int tmp;
	tmp = *x;
	*x = *y;
	*y = tmp;
}

int main()
{
	int a = 3;
	int b = 5;
	swap1(a, b); //值传递
	printf("a = %d, b = %d\n", a, b);

	a = 3;
	b = 5;
	swap2(&a, &b); //地址传递
	printf("a2 = %d, b2 = %d\n", a, b);

	return 0;
}

输出:

x = 5, y = 3
a = 3, b = 5
a2 = 5, b2 = 3

数组名做函数参数

数组名作为函数参数时退化为指针,传递的实际是数组首元素的地址(即:指针)而不是整个数组的副本。

因此:函数内部可以通过指针访问和修改数组的内容。

这意味着:函数内对数组的修改会影响原数组


数组名作为函数参数的语法:

//以下两种写法完全等价:
返回值类型 函数名(数据类型 数组名[], int 数组大小);
返回值类型 函数名(数据类型 *数组名, int 数组大小); 
//以下两种写法完全等价:
void printArray(int arr[], int n); // 数组名作为参数
void printArray(int *arr, int n);  // 指针作为参数

数组名作为函数参数的使用:

1.访问数组元素

  • 在函数内部,可以通过指针或下标访问数组元素。

    void printArray(int arr[], int n) 
    {
        for (int i = 0; i < n; i++) 
        {
            printf("%d ", arr[i]); // 通过下标访问数组元素
        }
        printf("\n");
    }
      
    int main() 
    {
        int arr[] = {1, 2, 3, 4, 5};
        int n = sizeof(arr) / sizeof(arr[0]);
        printArray(arr, n); // 传递数组名
      
        return 0;
    }
    

2.修改数组元素

  • 在函数内部,可以通过指针或下标修改数组元素。

    void modifyArray(int *arr, int n) 
    {
        for (int i = 0; i < n; i++) 
        {
            arr[i] *= 2; // 修改数组元素的值
        }
    }
      
    int main() 
    {
        int arr[] = {1, 2, 3, 4, 5};
        int n = sizeof(arr) / sizeof(arr[0]);
        modifyArray(arr, n); // 传递数组名
      
        for (int i = 0; i < n; i++) 
        {
            printf("%d ", arr[i]); // 输出:2 4 6 8 10
        }
        return 0;
    }
    

数组名作为函数参数的注意事项:

  1. 数组长度需额外传递

    • 数组名作为函数参数时,函数内部无法通过指针获取数组长度,因此通常需要额外显式传递数组的大小。

      void printArray(int arr[], int n) 
      

所以

  • 整数数组 做函数参数时,我们通常在函数定义中,封装2个参数。
    • 一个表示整数数组首地址,一个表示数组元素个数
  • 字符串(字符数组) 做函数参数时,不需要提供2个参数。 因为每个字符串都有'\0'
  • 字符串数组 做函数参数时,我们通常在函数定义中,封装2个参数。
    • 一个表示字符串数组首地址,一个表示数组元素个数

代码示例:在函数参数中数组名会退化为指针

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

void BubbleSort(int arr[])  // void BubbleSort(int *arr)
{
    printf("BubbleSort: sizeof(arr) = %d\n", sizeof(arr));
    printf("BubbleSort: sizeof(arr[0]) = %d\n", sizeof(arr[0]));

    int n = sizeof(arr) / sizeof(arr[0]);
    printf("BubbleSort: n = %d\n", n);


    for (int i = 0; i < n - 1; i++)
    {
        for (int j = 0; j < n - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}


int main()
{
    int a = 10;
    int* p = &a;
    printf("该平台指针所占的字节数为:%d\n", sizeof(p));

    int arr[] = { 5, 89, 3, 22, 40, 31, 9, 22, 67, 28, 45, 78 };
    printf("main: sizeof(arr) = %d\n", sizeof(arr));
    printf("main: sizeof(arr[0]) = %d\n", sizeof(arr[0]));

    int n = sizeof(arr) / sizeof(arr[0]);
    printf("main: n = %d\n", n);

    BubbleSort(arr);
    printf("冒泡排序后的结果为:\n");
    for (size_t i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

输出

该平台指针所占的字节数为:8
main: sizeof(arr) = 48
main: sizeof(arr[0]) = 4
main: n = 12
BubbleSort: sizeof(arr) = 8
BubbleSort: sizeof(arr[0]) = 4
BubbleSort: n = 2
冒泡排序后的结果为:
5 89 3 22 40 31 9 22 67 28 45 78

分析

1.main函数

  • 指针大小printf("该平台指针所占的字节数为:%d\n", sizeof(p));

    • 这里 sizeof(p) 返回的是指针 p 的大小,通常是 8(64 位系统)或 4(32 位系统)
  • 数组大小计算int n = sizeof(arr) / sizeof(arr[0]);

    • sizeof(arr) 返回的是数组 arr 的总大小。
    • sizeof(arr[0]) 返回的是单个元素的大小。
    • n 的值将是数组 arr 的元素个数,即 12
  • 调用 BubbleSort 函数BubbleSort(arr);

    • 由于 BubbleSort 函数内部的 n 计算错误,排序不会正确执行。

    • 由于排序未正确执行,打印的数组将是原始数组的顺序。

2.BubbleSort函数

  • 数组大小计算int n = sizeof(arr) / sizeof(arr[0]);
    • sizeof(arr) 返回的是指针的大小(8 或 4),而不是数组的大小。
    • 因为 arr 作为函数参数时,数组名会退化为指针,所以实际上是一个指针(int*),而不是数组。
    • sizeof(arr[0]) 返回的是单个元素的大小。(int 类型的大小)
    • n 的值将是 2(64 位系统)或 1(32 位系统),这显然不是数组的实际大小。

代码示例:数组名作为函数参数时额外传递数组长度

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

void BubbleSort(int arr[], int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        for (int j = 0; j < n - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main()
{
    int a = 10;
    int* p = &a;
    printf("该平台指针所占的字节数为:%d\n", sizeof(p));

    int arr[] = { 5, 89, 3, 22, 40, 31, 9, 22, 67, 28, 45, 78 };
    printf("main: sizeof(arr) = %d\n", sizeof(arr));
    printf("main: sizeof(arr[0]) = %d\n", sizeof(arr[0]));

    int n = sizeof(arr) / sizeof(arr[0]);
    printf("main: n = %d\n", n);

    BubbleSort(arr, n);
    printf("冒泡排序后的结果为:\n");
    for (size_t i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

输出

该平台指针所占的字节数为:8
main: sizeof(arr) = 48
main: sizeof(arr[0]) = 4
main: n = 12
冒泡排序后的结果为:
3 5 9 22 22 28 31 40 45 67 78 89
//由于数组名作为函数参数时额外传递数组长度n,n值为正确的值
//所以冒泡排序函数排序正确

指针作为函数的参数

指针作为函数参数的应用:

1.修改普通变量的值

  • 通过指针可以在函数中修改普通变量的值。

    void increment(int *p) 
    {
        (*p)++; // 修改指针指向的值
    }
            
    int main() 
    {
        int a = 10;
        increment(&a); // 传递a的地址
        printf("a = %d\n", a); // 输出:a = 11
        return 0;
    }
    

2.动态内存分配

  • 通过指针可以在函数中动态分配内存,并将分配的内存地址返回给调用者。

    void allocateMemory(int **p) 
    {
        *p = (int *)malloc(sizeof(int)); // 分配内存
        **p = 100; // 修改指针指向的值
    }
            
    int main() 
    {
        int *p = NULL;
        allocateMemory(&p); // 传递指针的地址
        printf("*p = %d\n", *p); // 输出:*p = 100
                
        free(p); // 释放内存
        return 0;
    }
    

3.修改数组元素

  • 通过指针可以在函数中修改数组元素的值。

    void modifyArray(int *arr, int n) 
    {
        for (int i = 0; i < n; i++) 
        {
            arr[i] *= 2; // 修改数组元素的值
        }
    }
            
    int main() 
    {
        int arr[] = {1, 2, 3, 4, 5};
        int n = sizeof(arr) / sizeof(arr[0]);
        modifyArray(arr, n); // 传递数组名(即数组首元素的地址)
                
        for (int i = 0; i < n; i++) 
        {
            printf("%d ", arr[i]); // 输出:2 4 6 8 10
        }
        return 0;
    }
    

指针作为函数的返回值

指针作为函数的返回值:

  • 指针作为函数的返回值时,返回的指针不能指向局部变量

    • 因为:局部变量在函数返回后会被销毁
    int *createArray(int n) 
    {
        int *arr = (int *)malloc(n * sizeof(int)); // 动态分配内存
        for (int i = 0; i < n; i++) 
        {
            arr[i] = i + 1;
        }
        return arr; // 返回动态分配的数组
    }
      
    int main() 
    {
        int *arr = createArray(5);
        for (int i = 0; i < 5; i++) 
        {
            printf("%d ", arr[i]); // 输出:1 2 3 4 5
        }
        free(arr); // 释放内存
        return 0;
    }
    

代码示例:指针作为函数的返回值的注意事项

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

int m = 100;       // 全局变量   
int* test_func1(int a, int b)
{
    return &m;
}

int* test_func2(int a, int b)
{
    int n = 1234;  // 局部变量
    return &n;
}

int main(void)
{
    int* ret1 = NULL;  // NULL == 0
    int* ret2 = NULL;  // NULL == 0

    ret1 = test_func1(10, 20);
    ret2 = test_func2(10, 20);

    printf("ret1 = %d\n", *ret1);
    printf("ret1 = %d\n", *ret1);
    printf("ret1 = %d\n", *ret1);

    printf("ret2 = %d\n", *ret2);
    printf("ret2 = %d\n", *ret2);
    printf("ret2 = %d\n", *ret2);

    system("pause");
    return EXIT_SUCCESS;
}

输出

ret1 = 100
ret1 = 100
ret1 = 100    
ret2 = -858993460  // 未定义行为,可能打印 1234(残留值)
ret2 = -858993460  // 未定义行为,可能打印 -858993460 或其他值
ret2 = -858993460  // 未定义行为,可能打印 -858993460 或其他值

分析

int m = 100;       // 全局变量   
int* test_func1(int a, int b)
{
    return &m;
}
  1. 全局变量m存储在 全局数据区,生命周期与程序相同。

  2. 函数 test_func1 返回 m 的地址时,这个地址在程序运行期间始终有效。

  3. 因此 ret1 指向的是一个有效的内存地址。

int* test_func2(int a, int b)
{
    int n = 1234;  // 局部变量
    return &n;
}
  1. 局部变量n存储在栈上,生命周期仅限于 test_func2 的执行期间。
  2. 函数 test_func2 返回 n 的地址时,n 的内存空间会被释放掉,返回的指针指向的内存区域不再有效。
  3. 因此 ret2 指向的是一个无效的内存地址。

👨‍💻 博主正在持续更新C语言基础系列中。
❤️ 如果你觉得内容还不错,请多多点赞。

⭐️ 如果你觉得对你有帮助,请多多收藏。(防止以后找不到了)

👨‍👩‍👧‍👦 C语言基础系列 持续更新中~,后续分享内容主要涉及 C++全栈开发 的知识,如果你感兴趣请多多关注博主。

相关文章:

  • Windows环境下Maven的配置
  • 【密码学——基础理论与应用】李子臣编著 第二章 古典密码 课后习题
  • <进程间通信>共享内存
  • 知微传感3D相机上位机DkamViewer使用:设置相机的静态IP
  • 量子计算:商业化应用的未来蓝图
  • linux server docker 拉取镜像速度太慢或者超时的问题处理记录
  • 双指针刷题和总结
  • C# OnnxRuntime部署DAMO-YOLO香烟检测
  • 安全运维:从防火墙到入侵检测
  • 测试工程师Ai应用实战指南简例prompt
  • EX_25/3/3
  • 基于YALMIP和cplex工具箱的IEEE33微电网故障处理电网重构matlab模拟与仿真
  • 国产NAS系统飞牛云fnOS深度体验:从运维面板到博客生态全打通
  • 【新人系列】Golang 入门(二):基本数据类型
  • graido学习记录
  • 【图论】判断图中有环的两种方法及实现
  • vi的常见操作命令
  • [数据结构] - - - 链表
  • 面试题02.01.移除重复节点
  • 【计算机网络03】网络层协议IP(详细)
  • 巴军事行动致印度70%电网瘫痪
  • 越怕出错越会出错,“墨菲定律”的魔咒该怎么破?
  • 盖茨:20年内将捐出几乎全部财富,盖茨基金会2045年关闭
  • 8小时《大师与玛格丽特》:长度可以是特点,但不是价值标准
  • 吴清:加强监管的同时传递监管温度,尽力帮助受影响企业应对美加征关税的冲击
  • 第四轮伊美核问题谈判预计5月11日举行