侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

C语言学习小记(10)-指针大杂烩

zze
zze
2024-03-21 / 0 评论 / 0 点赞 / 21 阅读 / 56034 字

不定期更新相关视频,抖音点击左上角加号后扫一扫右方侧边栏二维码关注我~正在更新《Shell其实很简单》系列

指针和内存的关系

内存是计算机中用来存储数据的一块区域,程序运行时,所有的变量、数组、结构体等数据都会被分配到内存中的某个位置,并且每个内存单元都有一个唯一的地址标识。

指针变量是用来存储内存地址的变量,它不直接存储数据本身,而是存储了指向数据所在内存地址的一个值。当你声明一个指针并初始化后,这个指针变量就包含了某个内存地址,通过解引用指针(使用 * 操作符)可以访问或修改该地址处的数据。

具体关系可以概括为以下几点:

内存地址

  • 内存中的每一个字节都有一个唯一的地址。

  • 变量在内存中分配空间时,系统会为它分配一段连续的内存,并返回其起始地址。

指针变量与内存空间

  • 指针变量在内存中也有自己的存储空间,占用大小通常是机器相关的,例如在32位系统上,指针通常占用4个字节,在64位系统上则占用8个字节。

  • 不同类型的指针(如 int char 等)尽管指向不同类型的数据,但它们作为指针变量自身所占的内存大小是相同的。

指向内存

  • 当你声明一个指针并将其初始化为某个变量的地址时,该指针就“指向”那个变量所在的内存。

  • 例如:int a = 10; int *p = &a; 这里,p 是一个指针变量,它现在保存的是整型变量 a 的内存地址。

操作内存

  • 使用指针可以直接对内存进行读写操作,无需知道变量的名字,只需要知道内存地址即可。

  • 例如:*p = 20; 这行代码将改变指针 p 所指向的内存位置(即变量 a 的位置)的内容为20

动态内存管理

  • 指针还可以用于动态分配内存,比如使用 malloc() 函数请求操作系统分配一块指定大小的内存,并返回这块内存的首地址给指针。

  • 对于这样的内存,直到显式调用 free() 函数释放之前,程序都可以通过指针来访问和管理这块内存。

指针和指针变量的关系

指针(Pointer)

  • 指针是一个抽象的概念,它代表内存中的一个地址。这个地址指向了存储在内存中某个数据对象的起始位置。

指针变量(Pointer Variable)

  • 指针变量是程序中定义的一个变量,用来存放指针值,即存储其他变量或数据结构在内存中的地址。

  • 在C语言中声明指针变量时,使用星号 来表示这是一个指针类型,例如 int p; 声明了一个可以存储整型变量地址的指针变量 p。

简而言之,指针是一种数据类型,而指针变量则是这种类型的变量实例。当你声明一个指针变量并初始化后,该变量就可以存储一个内存地址,通过解引用操作符 * 来访问或者修改该地址所指向的数据。因此,可以说指针变量是用来管理、操作和间接访问内存的一种工具。

示例

#include <stdio.h>

int main() {
    // 定义一个整型变量并赋值
    int num = 10;

    // 声明一个指向整型的指针变量(存储整型变量地址)
    int *p;

    // 将num变量的地址赋给指针变量p
    p = &num;

    // 输出指针变量p当前存储的地址
    printf("Address of num: %p\n", (void*)p);

    // 通过指针变量p访问其指向的内存中的数据(即num的值)
    printf("Value pointed by p: %d\n", *p);

    // 改变通过指针p访问到的数据(也就是改变num的值)
    *p = 20;

    // 再次输出num的值(现在应该已经改变)
    printf("New value of num: %d\n", num);

    return 0;
}

指针变量的初始化

指针变量是用来存储内存地址的特殊类型变量,为了正确使用指针,需要先将其初始化为一个有效的内存地址。

初始化的几种基本形式

赋值为NULL(空指针)

int *p;
p = NULL; // 或者 p = 0;

在C语言中,NULL 是预定义的一个宏,通常被定义为整数值0(void*)0。赋值为 NULL 后,p 不再指向任何有效的内存地址,表示这是一个空指针。这样做的好处是:

  • 在后续的程序逻辑中,可以通过检查 if (p == NULL) 来确定该指针是否已经指向了有效的内存区域。

  • 避免未初始化的指针导致的未定义行为,如访问无效内存地址引发的错误或崩溃。

(void*)0 表示一个空指针常量。这里的 void* 是一种通用指针类型,它可以指向任何类型的对象(或不指向任何对象),而 0 表示一个零值地址。

  • void*:这是一种特殊的指针类型,它并不指定具体的类型,可以用来存储任何类型对象的地址。这种类型通常用于内存管理、函数指针和需要处理多种数据类型的场景。

  • 0:在C语言中,数字0被解释为一个特殊地址,表示“无地址”或者“无效地址”。当用作指针时,它代表了一个不指向任何有效内存位置的指针值。

  • (void*)0:通过类型转换,将整数0强制转换成void*类型的指针,这样得到的就是一个类型安全的空指针常量。在C语言标准库中的宏定义NULL常常被实现为 (void*)0,以确保与各种数据类型的指针都能兼容,并且表示空指针的概念。

因此,在编程实践中,(void*)0 被广泛用于初始化指针变量到空指针状态,以及作为条件判断表达式的一部分来检查指针是否为空。例如:

int *p = (void*)0;  // 初始化指针 p 为 NULL 或空指针

if (somePointer == (void*)0) {
    printf("The pointer is NULL.\n");
}  // 检查 somePointer 是否为 NULL

这种初始化方式将指针设置为空指针,表示它目前不指向任何有效对象。在进行分配或释放内存、检查指针是否有效时,经常用到空指针。

赋值为其他变量的地址

int a = 10;
int *p;
p = &a; // 将指针p初始化为变量a的地址

这里,&a 是取址运算符,它返回变量 a 在内存中的地址,并将这个地址赋给指针变量 p。此时 p 指向了整型变量 a。

动态内存分配后赋值

int *p;
p = (int*)malloc(sizeof(int)); // 分配一块足够存放int类型的内存空间,并把首地址赋给p
if (p == NULL) {
    // 处理内存分配失败的情况
} else {
    *p = 20; // 现在可以通过p来访问和修改这块内存
}

使用 malloc() 函数动态地从堆上分配内存,然后将分配得到的内存首地址赋给指针变量。这样创建的内存区域可以在程序运行期间动态管理。

注意事项

  • 指针变量未初始化就直接使用是错误的,因为它的值是不确定的,可能指向无效内存位置,导致程序行为不可预测甚至崩溃。

  • 初始化指针时要确保它指向合法的、已经分配的内存空间,或者赋值为NULL以表示未指向有效对象。

  • 在不再需要动态分配的内存时,应调用 free(p) 来释放内存,防止内存泄漏。

指针变量的指向类型

取值宽度

指针变量的“指向类型”决定了它所指向的数据类型的大小(即取值宽度),而不是指针变量自身的存储大小。指针变量本身的大小是固定的,在32位系统上通常为4个字节,在64位系统上为8个字节,这个大小与指针指向的数据类型无关。

  • 当你声明一个指针变量时,例如 int p;这里的 p 是一个指针变量,它的指向类型是整型(int)。这意味着 p 可以存储整型变量的地址。

  • 虽然 p 作为指针本身的大小固定,但当你通过 p 解引用或间接访问数据时,读取或修改的是一个整型(在大多数编译器和平台上为4字节)大小的数据块。

  • 如果你声明了另一个指针 char c;,虽然 cp 都是4或8个字节(取决于平台),但是当通过 c 访问内存时,你会得到或改变一个字符型(1字节)的数据。

例 1

#include <stdio.h>

int main() {
    int num = 10;
    int *p = &num; // p是一个指向整型变量的指针
    char ch = 'A';
    char *c = &ch; // c是一个指向字符型变量的指针

    printf("Size of pointer to int: %ld bytes\n", sizeof(p)); // 输出指针p的大小
    printf("Size of pointer to char: %ld bytes\n", sizeof(c)); // 输出指针c的大小

    printf("Value stored at the address pointed by p (an integer): %d\n", *p); 
    printf("Value stored at the address pointed by c (a character): %c\n", *c);

    return 0;
}

在这个示例中,无论 p 指向的 int 类型变量还是 c 指向的 char 类型变量,它们作为指针变量本身的大小是一样的。然而,当你使用 pc 进行解引用操作时,你访问的数据宽度是不同的,一个是4字节的整数,另一个是1字节的字符。

例 2

// 定义一个整型变量num,并初始化为十六进制数0x01020304,假设int类型在当前系统中占用4个字节
int num = 0x01020304;

// 声明一个指向整型变量的指针p1,并将其初始化为num的地址
int *p1 = &num;
// 当我们通过*p1访问num时,会读取整个4字节的数据(即整个int值)
printf("%#x\n", *p1); // 输出:0x01020304

// 声明一个指向短整型变量的指针p2,但仍然将其初始化为num的地址
short *p2 = &num;
// 这里可能会导致未定义行为,因为试图用short类型的指针去访问一个int类型的对象。
// 实际运行结果取决于编译器和平台的具体实现:
// - 如果平台是小端字节序(Little Endian),*p2可能输出低两个字节的内容(例如0x0403);
// - 如果平台是大端字节序(Big Endian),*p2可能输出高两个字节的内容(例如0x0102)。
// 注意:这种不匹配类型的指针访问在C语言中是不推荐的,可能导致不可预测的结果。
printf("%#x\n", *p2); // 输出不确定,依赖于字节序和具体实现

// 声明一个指向字符型变量的指针p3,并将其初始化为num的地址
char *p3 = (char*)&num; // 同样地,这里的类型不匹配也可能导致未定义行为,此处仅用于演示
// 当执行 printf("%#x\n", *p3); 时,理论上会输出num的第一个字节内容,具体值取决于系统的字节序
printf("%#x\n", *p3); // 输出不确定,取决于字节序,0x01 或 0x04

// 我们可以通过循环来依次访问num所占内存中的每一个字节:
for (int i = 0; i < sizeof(num); ++i) {
    printf("%#x ", *(p3 + i)); // 分别输出num所对应的每个字节
}

// **重要提示**:
// 上述代码中的 short *p2 = &num; 和 char *p3 = &num; 是不规范的,
// 违反了C语言类型安全的原则,可能导致未定义的行为。
// 在实际编程中应避免这样的错误实践,确保指针操作合法且有意义。

加 1 的取值跨度

指针变量的指向类型决定了当你对指针执行加1操作时,它在内存中移动的跨度(即增加多少字节)。这是因为不同类型的变量在内存中占用的空间大小是不同的。

int num = 10;
short snum = 20;
char ch = 'A';

int *pInt = &num;
short *pShort = &snum;
char *pChar = &ch;

printf("Before: pInt = %p (in decimal: %ld)\n", (void *)pInt, (long)pInt);
pInt++;
printf("After +1: pInt = %p (in decimal: %ld)\n", (void *)pInt, (long)pInt); // 假设地址增加4字节

printf("Before: pShort = %p (in decimal: %ld)\n", (void *)pShort, (long)pShort);
pShort++;
printf("After +1: pShort = %p (in decimal: %ld)\n", (void *)pShort, (long)pShort); // 假设地址增加2字节

printf("Before: pChar = %p (in decimal: %ld)\n", (void *)pChar, (long)pChar);
pChar++;
printf("After +1: pChar = %p (in decimal: %ld)\n", (void *)pChar, (long)pChar); // 假设地址增加1字节

// 输出:
// Before: pInt = 0x16d4766bc (in decimal: 6128363196)
// After +1: pInt = 0x16d4766c0 (in decimal: 6128363200) // 地址增加了4字节
//
// Before: pShort = 0x16d4766ba (in decimal: 6128363194)
// After +1: pShort = 0x16d4766bc (in decimal: 6128363196) // 地址增加了2字节
//
// Before: pChar = 0x16d4766b9 (in decimal: 6128363193)
// After +1: pChar = 0x16d4766ba (in decimal: 6128363194) // 地址增加了1字节

综合示例

int num = 0x01020304;
short *p1 = (short *)&num;
// 取 0x0102
printf("%#x\n", *(p1 + 1)); // 0x0102
// 取 0x02
char *p2 = (char *)&num;
printf("%#x\n", *(p2 + 2)); // 0x02
// 取 0x0203
char *p3 = (char *)&num;
printf("%#x\n", *(short *)(p3 + 1)); // 0x0203

万能指针

void * 类型的指针被称作万能指针或通用指针,它不直接关联任何特定类型的数据。这意味着它可以指向任何类型的对象,但是不能直接对所指向的对象进行解引用操作,除非先将其显式转换为正确的数据类型。

声明与赋值

void *p; // 声明一个void指针
int num = 10;
p = &num; // 可以将任意类型变量的地址赋给void指针

类型转换

要从void * 指针访问实际数据,必须通过类型转换将其转换回原始类型。

printf("%d", *(int *)p); // 需要将void指针转换成int指针后才能解引用

内存分配与释放

在动态内存管理中,malloc()calloc()realloc()free() 函数都使用void *作为参数和返回值,这样可以用于分配和释放不同大小和类型的内存块。 

void *memory = malloc(sizeof(int)); // 分配一块足够存储整型数据的内存
if (memory) {
    int *pInt = (int *)memory; // 将分配到的内存转换为int指针并使用
    *pInt = 42;
    free(memory); // 释放内存时无需转换,因为free()接受void*参数
}

注意点

  • 使用 void * 指针需要谨慎处理类型转换,确保转换前后类型匹配,否则可能导致未定义行为或运行时错误。

  • void * 指针不能直接进行算术运算(如加减)或比较操作,需要转换为具体类型后再进行这些操作。

  • 在某些编译器下,尽管可以通过类型转换把任意类型的数据赋给 void * 指针,但这种做法违反了C语言类型安全的原则,应当尽量避免,并在必要时使用明确类型的安全方法。

数组与指针

在C语言中,数组名实际上就是指向数组首元素的常量指针。因此,可以通过数组名来获取数组元素的地址,并可以声明和初始化指向数组元素的指针变量。

数组与指针的关系

int arr[5] = {1, 2, 3, 4, 5};
// 在C语言中,arr和&arr[0]是等价的,它们都表示数组首元素的地址。

定义并初始化

int arr[] = {10, 20, 30, 40, 50};
int *ptr; // 声明一个指向整型数据的指针变量

ptr = arr; // 将arr(即数组首元素的地址)赋给ptr
或者
ptr = &arr[0]; // 显式地将数组第一个元素的地址赋给ptr

通过指针访问数组元素

printf("The first element is: %d\n", *ptr); // 输出数组的第一个元素值
ptr++; // 指针向后移动一位,指向下一个元素
printf("The second element is: %d\n", *ptr); // 输出数组的第二个元素值

多维数组元素的指针

int arr2[3][4];

int (*row)[4]; // 声明一个指向具有四个整型元素的一维数组的指针变量
row = arr2; // 指针指向二维数组的第一行

int *col; // 声明一个指向单个整型的指针变量
col = *arr2; // col现在指向二维数组的第一个元素
col = &arr2[0]; // col 现在指向 arr2[3] 一维数组的第一个元素
col = &arr2[0][0]; // 同样指向二维数组的第一个元素

你可以通过指针变量间接地访问和操作数组元素。

中括号的原理

在C语言中,[] 用于访问数组元素,其左边是数组名或指向数组元素的指针变量,右边是索引(即数组下标)。例如:

int arr[5] = {1, 2, 3, 4, 5};
int element = arr[2]; // 访问数组第三个元素(索引从0开始)

* 是指针解引用运算符,当它用于指针变量时,表示获取指针所指向内存地址的内容。例如:

int num = 10;
int *ptr = &num; // ptr 指向整型变量 num
int value = *ptr; // 解引用 ptr 获取 num 的值

结合理解一下,虽然二者语法形式不同,但在访问数组元素时,我们可以将数组名视为一个隐式的指针常量,其值就是数组首元素的地址。因此,访问数组元素 arr[i] 可以等价于使用指针解引用:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 将数组名赋给指针变量 p

// 下面两个语句等效
int element1 = arr[2];
int element2 = *(p + 2); // p + 2 计算出数组第三个元素的地址,然后通过 * 进行解引用

// 可以看做 [] 是对 *() 的缩写,[] 左边的值放在 + 的左边,[] 里面的值放在 + 的右边,整体取 *,所以下面两个语句等效
// 所以 arr[0] 等效于 *(arr + 0) 
int element3 = *(2 + arr);
int element4 = 2 [arr];

printf("Element1: %d\n", element1); // 3
printf("Element2: %d\n", element2); // 3
printf("Element3: %d\n", element3); // 3
printf("Element4: %d\n", element4); // 3

所以 &arr[0] 实际等效于 arr ,推导如下:

&arr[0] == &*(arr + 0) == arr + 0 == arr

数组指针

数组指针是C语言中一种特殊的指针,它指向整个数组,而非单个元素。在C语言中,数组名可以被视作一个常量指针,该指针指向数组的首地址(即第一个元素的地址)。但更准确地说,讨论“数组指针”时,我们通常是指指向数组类型的指针变量。

  • 定义和声明

// 假设有一个一维整型数组
int arr[5];

// 定义一个指向整型数组的指针变量,它能够指向任何长度为n的一维整型数组
int (*p)[5]; // 这是一个能存储一维数组地址的指针变量,数组中的元素都是int类型且有5个

// 或者,声明并初始化一个已知大小的数组指针
int (*p)[5] = &arr; // p现在指向了arr数组
  • 解引用与访问

通过数组指针访问元素的方式与普通指针有所不同。因为数组指针指向的是整个数组,所以需要两次解引用才能访问到具体元素:

// 对数组指针解引用,得到的是数组的首元素地址
// p = &arr => *p = arr => 数组的首元素地址
(*p)[0] = 10; // 访问并设置数组的第一个元素值为10

如果要让数组指针遍历整个数组,可以通过递增指针来实现,但是每次递增都会跳过数组长度那么多字节:

++p; // 现在p指向了下一个整型数组(假设每个数组之间是连续分配的)
  • 多维数组与数组指针

对于多维数组,数组指针的概念也可以扩展。例如,二维数组 int matrix[3][4],可以有一个指向其行的指针:

int (*ptr)[4] = matrix; // ptr是一个指向长度为4的整型数组的指针,它可以遍历matrix的所有行

在这里,对 ptr 加1将使其指向下一行(一个新的包含4个整数的数组)。

  • 演示数组指针加 1 的跨度

int arr[5] = {1, 2, 3, 4, 5};
printf("arr => %u\n", arr); // arr 指向首元素地址
printf("arr + 1 => %u\n", arr + 1);
printf("&arr => %u\n", &arr); // &arr 指向数组首地址,类型是数组指针
printf("&arr + 1 => %u\n", &arr + 1); // 所以它 +1 的跨度是整个数组的大小

// arr => 6164522656
// arr + 1 => 6164522660
// &arr => 6164522656
// &arr + 1 => 6164522676

如上,当通过 &arr 这种形式取数组地址时,它的地址指向位置和 arr 指向位置相同,都是指向数组第一个元素的地址,但是:

  • arr+1 这种加 1 的跨度是数组的一个元素大小;

  • &arr + 1 这种地址加 1 的跨度是整个数组的大小;

指针数组

指针数组是C语言中一种特殊的数组,它的元素都是指向某种类型数据的指针。指针数组可以存储一系列不同变量的地址。

与数组指针的区别

int (*p)[5]int *p[5] 的含义是不同的。

  • 数组指针:int (*p)[5]

这里定义了一个指向具有 5 个整型元素的一维数组的指针变量 p。当你对这个指针解引用时,它将返回一个可以存储5 个整数的数组。通过递增该指针(如 ++p),它会跳过整个数组(即5个整数)的大小。这种类型的指针通常用于处理动态分配的内存块或函数参数传递一维数组的情况。

  • 指针数组:int *p[5]

这里定义了一个包含 5 个元素的数组 p,每个元素都是一个指向整型的指针。这意味着你有一个由5个独立指针组成的数组,它们各自可以指向一个整数。通过递增数组中的某个指针(如 p[0]++),它将指向下一个整数地址;而通过递增数组下标(如 ++p),则会移动到数组中的下一个指针元素。

总结来说,int (*p)[5] 是一个数组指针,它指向的是整个一维数组;而 int *p[5] 是一个指针数组,其中包含了5个独立的指针。

它们写法太相似,说下快速辨别它们的方法:

  1. 观察括号的位置:

    1. 对于数组指针:int (*p)[5] 括号包裹了星号(*),这表明我们首先定义了一个指针,然后指明它指向的是一个具有5个整型元素的数组。

    2. 对于指针数组:int *p[5] 星号 (*) 没有被括号包围,而是在数组声明之后直接出现,表示这是一个数组,数组中的每个元素都是指向整型的指针。

  2. 记忆规则:

    1. 如果括号在星号前面并且紧挨着星号,如 T (*p)[N],那么它是一个指向大小为N的类型T数组的指针。

    2. 如果没有这样的括号或者括号在星号后面,如 T *p[N],则它是一个包含N个元素的指针数组,每个元素是指向类型T的指针。

示例

  • 例 1:数值指针数组

#include <stdio.h>

int main() {
    // 声明一个可以存储3个整型指针的指针数组
    int *numPtrArray[3];

    // 创建三个整数变量并初始化
    int num1 = 10;
    int num2 = 20;
    int num3 = 30;

    // 将这三个变量的地址分别赋给指针数组的元素
    numPtrArray[0] = &num1;
    numPtrArray[1] = &num2;
    numPtrArray[2] = &num3;

    // 通过指针数组访问和修改对应的变量值
    printf("Before: num1 = %d, num2 = %d, num3 = %d\n", *numPtrArray[0], *numPtrArray[1], *numPtrArray[2]);
    (*numPtrArray[1])++; // 通过指针数组增加num2的值

    printf("After: num1 = %d, num2 = %d, num3 = %d\n", *numPtrArray[0], *numPtrArray[1], *numPtrArray[2]);

    return 0;
}

在这个例子中,numPtrArray 是一个指针数组,它包含三个元素,每个元素都可以存储 int 类型变量的地址。我们通过这个数组能够间接地访问或修改所指向的整数变量。

  • 例 2:字符指针数组示例

// 声明一个可以存储3个字符串指针的指针数组
char *strPtrArray[3];

// 初始化三个字符串字面量
char str1[] = "Hello";
char str2[] = "World";
char str3[] = "!";

// 将这三个字符串首地址赋给指针数组的元素
strPtrArray[0] = str1;
strPtrArray[1] = str2;
strPtrArray[2] = str3;

// 通过指针数组访问和打印对应的字符串
printf("%s %s %s\n", strPtrArray[0], strPtrArray[1], strPtrArray[2]);

char *strPtrArray2[3] = {"Hello", "World", "!"};
// 通过指针数组访问和打印对应的字符串
printf("%s %s %s\n", strPtrArray2[0], strPtrArray2[1], strPtrArray2[2]);

strPtrArray[1][1] = 'A';  // 可以修改
strPtrArray2[1][1] = 'A'; // 此行会出错,不能修改,因为实际数据在常量区

在这个例子中,strPtrArray 是一个字符指针数组,其元素是指向 char 类型的指针,通常用于处理字符串。这里我们用它来存放三个字符串的首地址,并通过该数组打印出这些字符串。

  • 例 3:二位字符数组

// 定义一个字符指针数组arr1,它包含3个元素,每个元素都是指向字符串字面量的指针。
// 字符串字面量存储在程序的常量区,不可修改。arr1中的指针分别指向这些字符串的首地址。
char *arr1[3] = {"Hello", "World", "!"};

// 定义一个二维字符数组arr2,它是一个3行1列(每行最大长度为128个字符)的数组。
// 每一行都可以存放一个字符串,并且可以修改这些字符串的内容。
// 注意:这里假设每个字符串都不超过127个字符加上结束符'\0',实际需要根据字符串的最大长度来调整第二维大小。
char arr2[3][128] = {"Hello", "World", "!"};

// 内存分配与内容可修改性:
//   arr1 中的元素是指针,它们指向的是存储在只读存储区域的字符串字面量,不能直接通过 arr1 修改这些字符串的内容。
//   arr2 是一个真正的字符数组,其中的数据存储在栈空间或静态存储区(取决于其声明的位置),可以通过下标访问和修改数组内的字符串内容。
// 存储方式:
//   arr1 本身存储的是3个指针,这些指针指向了不同的内存区域。
//   arr2 存储的是连续的内存块,其中包含了3个独立的、可容纳字符串的数组。
// 使用场景:
//   arr1 更适合用于引用不可变的字符串或者在函数之间传递字符串地址。
//   arr2 适用于动态构建和修改字符串,特别是在函数内部需要操作字符串时。

案例

通过指针遍历数组

  • 遍历一维数组

int arr[5] = {1, 2, 3, 4, 5};
// 方式一
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
    printf("%d\n", arr[i]);
}
// 方式二
int *p = arr;
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
    printf("%d\n", *(p + i));
}
// 方式三
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
    printf("%d\n", *(arr + i));
}
  • 遍历二维数组

int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int rows = sizeof(arr) / sizeof(arr[0]);
int cols = sizeof(arr[0]) / sizeof(arr[0][0]);

printf("%d\n", *(*(arr + 1) + 2)); // 7
printf("%d\n", arr[1][2]); // 7
for (int i = 0; i < rows; i++)
{
    for (int j = 0; j < cols; j++)
    {
        // 取第 i 行的第 j 列
        printf("%d ", *(*(arr + i) + j));
    }
    printf("\n");
}

int *firstNum = &arr[0][0];
for (int i = 0; i < rows * cols; i++)
{
    printf("%d ", *(firstNum + i));
}

求值练习

  • 分别求 p[-1]p[1] 的值:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr + 3;
// p[-1] = *(p - 1) = *(arr + 3 - 1) = *(arr + 2)
printf("%d\n", p[-1]); // 3
// p[1] = *(p + 1) = *(arr + 3 + 1) = *(arr + 4)
printf("%d\n", p[1]); // 5
  • a[*(a + a[3])] 的值:

int a[] = {8, 1, 2, 5, 0, 4, 7, 6, 3, 9};
// a[*(a + a[3])) = *(a + *(a + a[3])) = *(a + *(a + *(a + 3))) = *(a + *(a + 5)) = *(a + 4) = 0
printf("%d", a[*(a + a[3])]); // 0
  • *((int *)(p + 1) - 2) 的值:

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

printf("%d", *((int *)(p + 1) - 2)); // 4
// (p + 1) => 指针跳到数组末尾
// (int *) => 将指针强转为 int *
// 此时 -2 指针跨度将使用 int 的跨度,也就会指向倒数第二个元素
// 最后解引用使用的类型还是 int *,所以值就是倒数第二个元素,结果就是 4

字符串与指针

字符数组和字符串指针变量是处理文本数据(即字符串)的两种不同方式。

字符数组

定义与初始化

字符数组是一个用于存储多个字符的固定大小数组。声明时需要指定数组的长度。

char strArray[20]; // 声明一个可以容纳19个字符加上结束符'\0'的字符数组

初始化可以同时进行:

char strArray[20] = "Hello, World!"; // 自动添加'\0'作为结束符

操作

  • 通过下标访问和修改数组中的单个字符。

  • 使用字符串处理函数如strlen()strcpy()等。

特点

  • 内存空间在编译时分配,大小不可变。

  • 存储的是连续的字符序列,并以空字符'\0'作为字符串结束标志。

字符串指针变量

定义与初始化

字符串指针变量本质上是一个指向字符类型的指针,它存储的是字符地址而不是字符本身。

char *strPtr; // 声明一个指向char的指针变量

初始化可以指向一个字符数组或字符串字面量的首地址:

char strArray[] = "Hello";
char *strPtr = strArray; // 指向字符数组

或者直接指向字符串字面量:

char *strPtr = "World";

操作

  • 通过解引用 *strPtr 访问当前指向的字符。

  • 使用自增或自减运算符改变指针位置来遍历字符串。

  • 可以动态地更改指针所指向的字符串。

特点

  • 字符串指针变量更灵活,可指向不同长度的字符串。

  • 字符串字面量常驻内存,在程序执行期间不能被修改。

  • 动态分配内存得到的字符串(如用malloc()函数),其内存需手动释放(用free()函数)。

示例

#include <stdio.h>

int main() {
    // 字符数组示例
    char arr[20] = "Hello, array!";
    printf("%s\n", arr); // 输出:Hello, array!

    // 修改数组内容
    arr[6] = ', ';
    printf("%s\n", arr); // 输出:Hello, array!

    // 字符串指针示例
    char *ptr = "Hello, pointer!";
    printf("%s\n", ptr); // 输出:Hello, pointer!

    // 改变指针指向
    ptr = "Another string.";
    printf("%s\n", ptr); // 输出:Another string.

    return 0;
}

区别

char str1[] = "hello world";
char *str2 = "hello world";

这两行C语言代码的区别主要在于内存分配方式和字符串内容的可修改性:

内存分配方式

  • char str1[] = "hello world"; 这行代码定义了一个字符数组str1,并将其初始化为字符串字面量"hello world"。编译器会在栈空间(对于局部变量)或静态存储区(对于全局变量或静态变量)中为数组分配足够的空间来存储整个字符串,包括结束符'\0'

  • char *str2 = "hello world"; 这行代码声明并初始化了一个指向字符类型的指针str2,它被设置为指向一个存储在只读存储区(常量区)中的字符串字面量"hello world"。这个字符串字面量是静态分配的,程序运行时不可修改,并且所有在同一位置出现的相同字符串字面量都会共享同一块内存区域。

字符串内容的可修改性

  • 对于str1,因为它是一个字符数组,你可以通过数组下标访问和修改其中的字符。

str1[0] = 'H'; // 正确,可以修改字符数组的内容
  • 而对于str2,由于它指向的是常量存储区中的数据,尝试修改该地址处的值将导致未定义行为,甚至可能引发段错误:

str2[0] = 'H'; // 错误,试图修改字符串字面量,可能导致段错误或未定义行为

总结起来,str1是一个可以在程序执行期间修改其内容的字符数组,而str2则是一个指向固定、不可修改的字符串字面量的指针。

指针与函数

指针变量作为函数参数

指针变量作为函数参数传递时,实际上是将指针所存储的地址值(即指向的内存位置)作为参数传入函数。这意味着通过指针,函数可以访问和修改指针所指向的原始数据。

值传递与引用传递

  • C语言中的函数参数默认是值传递,但通过指针,我们可以实现类似“引用传递”的效果。

  • 当一个指针作为参数传递给函数时,函数接收到的是指针本身(地址值),而不是它所指向的数据的副本。

改变原数据

  • 在函数内部对指针进行解引用或修改指针指向的位置,会直接影响到函数外部的原数据。

示例代码

#include <stdio.h>

// 函数声明,接受一个整型指针作为参数
void modifyValue(int *ptr) {
    // 修改指针所指向的值
    (*ptr)++;  // 等价于 ptr[0]++;
}

int main() {
    int num = 5;
    
    // 将num的地址传递给函数
    modifyValue(&num);

    // 输出结果,num的值已经被修改
    printf("The value of num after calling modifyValue: %d\n", num); // 输出6

    return 0;
}

在这个例子中,modifyValue函数接收一个整型指针作为参数,当我们在main函数中调用此函数并传入&num时,实际上传递的是变量num的地址。在modifyValue函数内部,我们可以通过解引用操作符*来修改这个地址处存储的值,这样就直接改变了main函数中num的值。

注意点

  • 在使用指针作为函数参数时,必须确保指针已经初始化,并且指向有效的内存区域。

  • 如果函数内部需要动态分配内存,则需考虑内存管理问题,如是否由调用方负责释放内存等。

数组作为函数参数

值传递与地址传递

当一个数组被用作函数参数时,实际上传递的是数组的首地址(即第一个元素的地址)而不是整个数组的内容。这是因为数组名在表达式上下文中会被自动转换为指向数组首元素的指针。

形参声明

在函数定义时,可以使用以下几种等价的方式来声明一维数组作为形参:

void func(int arr[5]); // 声明了一个长度为5的一维整型数组
void func(int arr[]); // 省略长度的声明方式,编译器可以根据实参推断长度
void func(int *arr);  // 使用指针的方式声明,等效于前两种

修改实参的影响

  • 函数内部可以通过指针操作修改数组元素的值,这些改变会反映到传入函数的实际数组上。

  • 但是,不能通过函数来改变实参数组本身的大小或重新分配内存。

示例代码

  • 修改数组元素值

#include <stdio.h>

// 定义一个函数,接收一维整型数组
void modifyArray(int arr[]) {
    for (int i = 0; i < 5; ++i) {
        arr[i] *= 2; // 修改数组元素的值
    }
}

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

    // 调用函数并传入数组
    modifyArray(numbers);

    // 输出结果,numbers数组的值已经被修改
    for (int i = 0; i < 5; ++i) {
        printf("%d ", numbers[i]);
    }

    return 0;
}

在这个例子中,modifyArray函数接收一个一维整型数组作为参数。虽然我们在声明时写的是int arr[],但其实质上相当于传递了int *arr,也就是数组首元素的地址。因此,在函数内部对数组元素的修改会影响到主函数中的numbers数组。

  • 求最大值

void inputToIntArr(int *arr, int n)
{
    printf("输入 %d 个数:\n", n);
    for (int i = 0; i < n; i++)
    {
        printf("请输入第 %d 个数: \n", i + 1);
        scanf("%d", arr + i);
    }
    return;
}

void getMaxMin(int *arr, int n, int *maxResult, int *minResult)
{
    int max = arr[0], min = arr[0];

    for (int i = 1; i < n; i++)
    {
        // printf("min: %d, max: %d, arr[%d]: %d\n", min, max, i, arr[i]);
        if (max < arr[i])
        {
            max = arr[i];
        }

        if (min > arr[i])
        {
            min = arr[i];
        }
    }
    *maxResult = max;
    *minResult = min;

    return;
}

int main()
{
    int arr[5] = {};
    int n = sizeof(arr) / sizeof(arr[0]);
    int max = 0, min = 0;
    inputToIntArr(arr, n);
    getMaxMin(arr, n, &max, &min);
    printf("max: %d, min: %d", max, min);
    return 0;
}

编译器优化

编译器确实会对数组参数进行处理,将其视为指针,但在源代码层面并不需要程序员显式地将数组转为指针。无论你在函数签名中如何声明,当数组作为参数传递时,编译器都会以相同的方式处理:传递数组的首地址给函数。这是C语言的标准行为,并非特殊的优化措施。

二维数组作为参数

二维数组作为函数参数时,我们需要明确传递行数和列数信息,因为编译器不能从实参推断出二维数组的尺寸。

对于二维数组作为函数参数,我们也可以使用指针来表示。二维数组的元素其实就是一维数组,因此我们可以传递一个指向一维数组(即数组的行)的指针(二维数组第一个元素的指针),并另外传递行数和列数。

#include <stdio.h>

// 定义一个函数,接收一个指向一维整型数组的指针及其尺寸
void modify2DArray(int (*arr)[3], int rows) {
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < 3; ++j) {
            // 修改数组元素的值
            (*arr)[i][j] *= 2;
        }
    }
}

int main() {
    int numbers[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

    // 调用函数并传入指向二维数组首行的指针及其行数
    modify2DArray(numbers, 3);

    // 输出结果,numbers数组的值已经被修改
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("%d ", numbers[i][j]);
        }
        printf("\n");
    }

    return 0;
}

在这个例子中,modify2DArray函数现在接受一个指向长度为3的一维整型数组的指针以及行数作为参数。虽然声明时使用了int (*arr)[3]的形式,但在调用函数时,我们仍然直接传递原始二维数组名numbers,因为数组名在这里自动转换为指向其首行的指针。函数内部对元素的访问方式不变,但形参定义更清晰地体现了实际的数据结构。

函数指针

函数指针是一种特殊的指针,它存储的是一个函数的地址。通过函数指针,你可以像操作其他指针一样调用函数,使得程序更加灵活,特别是在处理回调函数、函数表、策略模式等场合非常有用。

函数指针的声明格式如下:

return_type (*function_pointer_name)(parameter_types);
  • return_type 是函数返回的类型。

  • function_pointer_name 是你要声明的函数指针变量的名字。

  • parameter_types 是函数参数的类型列表。

例如,声明一个指向无参数且返回整数的函数指针:

int (*fp)();

这意味着fp是一个可以指向任何返回整数且无参数的函数的指针。

示例代码

  • 案例1:声明并使用函数指针

#include <stdio.h>

// 定义一个求两数之和的函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 声明一个指向接受两个整数并返回整数的函数的指针
    int (*funcPtr)(int, int) = add;

    // 通过函数指针调用add函数
    int result = (*funcPtr)(5, 10);
    printf("Result: %d\n", result);  // 输出:15

    // 也可以简化为:
    int anotherResult = funcPtr(3, 7);
    printf("Another Result: %d\n", anotherResult);  // 输出:10

    return 0;
}
  • 案例2:函数指针作为参数

#include <stdio.h>

// 接受一个函数指针作为参数的函数
void applyFunction(int x, int (*operation)(int), int y) {
    int result = operation(x);
    printf("Applied function on %d with y = %d: %d\n", x, y, result);
}

// 定义两个待选函数
int multiply(int a) {
    return a * 2;
}

int divideByTwo(int a) {
    return a / 2;
}

int main() {
    int y = 3;
    
    // 将函数指针作为参数传递
    applyFunction(10, multiply, y);  // 20
    applyFunction(10, divideByTwo, y); // 5

    return 0;
}

在这个例子中,applyFunction函数接收一个整数和一个函数指针作为参数,调用该函数指针处理输入的整数。在main函数中,我们分别传递了multiplydivideByTwo函数的地址给applyFunction函数。

  • 案例3:使用 typedef 定义函数指针类型

#include <stdio.h>

// 使用typedef定义函数指针类型
typedef int (*NumFunc)(int);

// 接受一个函数指针作为参数的函数
void applyFunction(int x, NumFunc operation, int y) {
    int result = operation(x);
    printf("Applied function on %d with y = %d: %d\n", x, y, result);
}

// 定义两个待选函数
int multiply(int a) {
    return a * 2;
}

int divideByTwo(int a) {
    return a / 2;
}

int main() {
    int y = 3;
    
    // 将函数指针作为参数传递
    applyFunction(10, multiply, y);  // 注意在实际调用时需要提供y值
    applyFunction(10, divideByTwo, y);

    return 0;
}
  • 案例四:函数指针数组

#include <stdio.h>

// 定义计算函数原型
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);

// 函数指针类型定义
typedef int (*Calculator)(int, int);

// 计算函数实现
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

int divide(int a, int b) {
    if (b != 0) {
        return a / b;
    } else {
        printf("Error: Division by zero.\n");
        return -1; // 返回错误代码,或抛出异常(在C++中)
    }
}

// 函数指针数组
Calculator operations[] = {add, subtract, multiply, divide};
// int (*operations[])(int, int) = {add, subtract, multiply, divide};

int main() {
    int num1 = 10, num2 = 5, choice;
    printf("Choose an operation:\n");
    printf("1. Addition\n2. Subtraction\n3. Multiplication\n4. Division\n");

    scanf("%d", &choice);

    // 检查输入的有效性
    if (choice >= 1 && choice <= 4) {
        printf("Result: %d\n", operations[choice - 1](num1, num2));
    } else {
        printf("Invalid choice.\n");
    }

    return 0;
}

0

评论区