C语言:深入了解指针2(超详细)
1. 指针跟数组相关的知识
在 C 语言里,数组名在大多数表达式中会隐式转换为指向数组首元素的指针。不过,数组名和指针并不完全等同。
1.数组就是数组,是一块连续的空间,是可以存放一个或者多个数组的。
2.指针变量是一个变量,是可以存放地址的变量。
数组和指针不是一回事。
但是可以是一种指针来访问数组。
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
// 数组名arr隐式转换为指向数组首元素的指针
int *p = arr;//数组名字就是数组的地址
printf("arr[0]的值: %d\n", arr[0]);
printf("通过指针访问arr[0]的值: %d\n", *p);
return 0;
}
在上述代码中,arr
是一个整型数组,ptr
是一个整型指针。arr
在赋值给 p
时隐式转换为指向 arr[0]
的指针,因此可以通过 *p
访问 arr[0]
的值。
其实数组名就是数组首元素的地址(但有列外)
有2个列外:
1. sizeof(数组名):这里数组名并不表示首元素的地址,其实这里数组名表示整个数组,计算的是则是整个数组的大小单位是字节。
2. &数组名: 这里的数组名也表示整个数组,取出的是整个数组的地址。
除此之外,所有的数组名都是首元素的地址
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
printf("arr = %p\n", arr);
printf("arr + 1 = %p\n", arr + 1);
printf("arr = %p\n", &arr);
printf("&arr + 1 = %p\n", &arr+1);
return 0;
虽然说 &arr 取出来的跟&arr[0]和arr取出来的一样,但随着加1就可以看出,&arr取出的是整个数组的地址。
2. 使⽤指针访问数组
指针运算
1.数组在内存中是连续存放的。
2.指针的元素很方便的可以遍历数组,取出数组的内容
我们这里用代码展示:
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr; //arr = &arr[0]
int i = 0;
//输入
for (i = 0; i < sz; i++)
{
scanf("%d", p+i); // p + i = arr + i = &arr[i] 这里都表示下标元素的地址
//scanf("%d", arr + i);
}
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i)); // *(p+i) = arr[i] =*(arr +i)这些都表示对相应的元素取值
//printf("%d ", *(arr+ i));
}
return 0;
}
这里我们来详细讲解一下:p + i = arr + i = &arr [ i ] 这里都表示下标元素的地址,是完全等价
详细解释
1. arr + i
与 &arr[i]
相等
- 数组名的本质:在 C 语言中,数组名在大多数表达式中会隐式转换为指向数组首元素的指针。也就是说,
arr
本身代表了数组首元素arr[0]
的地址。 - 指针算术运算:当对指针进行加法运算时,如
arr + i
,它会根据指针所指向的数据类型的大小进行偏移。由于arr
是int
类型的数组,arr + i
会将指针从数组首元素地址向后偏移i * sizeof(int)
个字节,从而指向数组中第i
个元素的地址。 &arr[i]
的含义:arr[i]
是对数组中第i
个元素的引用,&
是取地址运算符,所以&arr[i]
表示获取数组中第i
个元素的地址。- 等价性:由于
arr + i
和&arr[i]
都表示数组中第i
个元素的地址,因此它们是相等的。
2. p + i
与 arr + i
相等
- 指针
p
的初始化:在前面的代码中,p
被初始化为p = arr
,这意味着p
存储的是数组arr
首元素的地址,即p
和arr
指向同一个位置。 - 指针算术运算的一致性:当对
p
进行p + i
的运算时,和arr + i
一样,它也会根据p
所指向的数据类型(这里是int
类型)进行偏移,向后偏移i * sizeof(int)
个字节,从而指向数组中第i
个元素的地址。 - 等价性:因为
p
和arr
初始时指向同一个地址,且它们进行相同的指针算术运算(加上i
),所以p + i
和arr + i
也指向同一个地址,即它们是相等的。
我们结合上面的再详细讲解一下:*(p+i) = arr[ i ] =*(arr + i )这些都表示对相应的元素取值,都是等价
详细解释
1. arr + i
与指针算术运算
- 在 C 语言中,数组名
arr
在大多数表达式里会隐式转换为指向数组首元素的指针,也就是说arr
代表了arr[0]
的地址。 - 当进行
arr + i
这样的操作时,这属于指针算术运算。因为arr
指向的是int
类型的数据,所以arr + i
会使指针从arr
所指向的位置向后偏移i * sizeof(int)
个字节,最终得到数组中第i
个元素的地址。
2. *(arr + i)
:对地址解引用
*
是解引用运算符,它的作用是获取指针所指向地址处存储的值。*(arr + i)
就是先通过arr + i
得到数组中第i
个元素的地址,然后对这个地址进行解引用操作,从而获取该地址处存储的元素值,也就是数组中第i
个元素的值。
3. arr[i]
与 *(arr + i)
的等价性
- 在 C 语言的语法设计中,
arr[i]
实际上是*(arr + i)
的一种语法糖。这是为了方便程序员更直观地访问数组元素而设计的。所以从本质上来说,arr[i]
和*(arr + i)
是完全等价的,它们都表示数组arr
中第i
个元素的值。
4. p + i
与 arr + i
的关系
- 由于
p
被初始化为指向数组arr
的首元素,所以p
和arr
指向的是同一个地址。 - 那么进行
p + i
操作时,和arr + i
一样,也是将指针从当前位置向后偏移i * sizeof(int)
个字节,最终得到数组中第i
个元素的地址。
5. *(p + i)
:对 p + i
解引用
- 同样,
*(p + i)
是对p + i
这个地址进行解引用操作,获取该地址处存储的值,也就是数组中第i
个元素的值。所以*(p + i)
与*(arr + i)
以及arr[i]
是等价的。
重点:
其实总结上面的我们可以理解:arr[ i ] == *( arr + i ) ,为什么arr恒等于&arr[0],其实&arr[0]可以写成&*(arr + 0),在这里说一下&和*这二个只要相遇就可以可以抵消(本质上是因为它们分别是取地址和解引用这一对相反操作),也就是说:
&arr[ 0 ] = &*(arr + 0) = arr + 0 = arr
他们也是支持加法结合律如:arr[ 1 ] = *( arr + 1 )= 1[ arr ] =*( 1 + arr )
3. ⼀维数组传参的本质
1.在函数调用时,一维数组作为参数传递给函数有以下几种常见形式,但本质都是相同的。
形式一:以数组形式声明参数
#include <stdio.h>
// 函数以数组形式声明参数
void printArray1(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);//计算元素个数
printArray1(arr, size);//传递 数组地址 和 求的数组数量
return 0;
}
在这个例子中,printArray1
函数的参数 arr
看起来是一个数组,但实际上在函数内部它会被当作指针来处理。
形式二:以指针形式声明参数
#include <stdio.h>
// 函数以指针形式声明参数
void printArray2(int *arr, int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
printArray2(arr, size);
return 0;
}
这里 printArray2
函数的参数 arr
直接声明为指针,它接收的也是数组首元素的地址。
2. 传参本质是传递数组首元素地址
- 数组名的隐式转换:在 C 语言里,数组名在大多数表达式中会隐式转换为指向数组首元素的指针。当把数组作为参数传递给函数时,传递的不是整个数组,而是数组首元素的地址。例如在上面的代码中,
arr
作为参数传递给printArray1
或printArray2
函数时,实际传递的是&arr[0]
。 - 函数内部处理:在函数内部,无论是以
int arr[]
还是int *arr
形式声明参数,编译器都会将其当作指针来处理。因此,在函数中对arr
进行的操作实际上是对指针进行的操作。例如arr[i]
等价于*(arr + i)
,都是通过指针偏移来访问数组元素。
3. 不能在函数内部获取数组长度
由于传递的只是数组首元素的地址,函数并不知道数组的长度。所以在函数参数中通常需要额外传递一个表示数组长度的参数,像上面例子中的 size
。例如以下错误示例:
#include <stdio.h>
// 错误示例:无法在函数内部正确获取数组长度
void wrongPrintArray(int arr[])
// 这里int arr[] == int* arr, []里面其实放不放都没有影响,放了编译器也不会读,放多少都是一样
{
int size = sizeof(arr) / sizeof(arr[0]);
// 1 4 / 4
printf("%d", size);//这里我们打印出来就是1
// 得出的结果跟环境是有很大的关系的,
// 这里演示的是vs2022 x86 环境下运行的
}
int main()
{
int arr[] = {1, 2, 3, 4, 5};
wrongPrintArray(arr);
return 0;
}
详细讲解:
1. 数组作为参数传递时的本质
在 C 语言中,当数组作为参数传递给函数时,数组会 “退化” 为指向其首元素的指针。也就是说,在wrongPrintArray
函数中,int arr[]
实际上等同于int *arr
。
2. sizeof
运算符在函数内部的作用
sizeof(arr[0])
:arr[0]
是一个int
类型的元素,在常见的系统中,int
类型通常占用 4 个字节,所以sizeof(arr[0])
的值为 4。sizeof(arr)
:由于arr
已经退化为指针,sizeof(arr)
计算的是指针的大小。在 32 位系统中,指针大小为 4 个字节;在 64 位系统中,指针大小为 8 个字节。这里假设你使用的是 64 位系统,所以sizeof(arr)
的值为 8。int size = sizeof(arr) / sizeof(arr[0]);
:根据前面的分析,sizeof(arr)
为 8,sizeof(arr[0])
为 4,所以size
的值为8 / 4 = 2
。这与预期的数组长度 5 不符,因为arr
已经退化为指针,无法通过sizeof
运算符正确获取数组的长度。
4. ⼆级指针
定义和基本概念
指针是一个变量,它存储的是另一个变量的内存地址。而二级指针则是存储指针变量的内存地址,其他的三级指针,四级指针......等,都是一样的。
- 普通指针的定义形式:
类型 *指针变量名;
- 二级指针的定义形式:
类型 **二级指针变量名;
int a = 10;
int *pa = &a; // 一级指针,指向int类型变量a
int **ppa = &pa; // 二级指针,指向一级指针pa
在上述代码中,pa
是一个一级指针,它存储了a
的地址;ppa
是一个二级指针,它存储了pa
的地址。
使用场景
- 动态二维数组:在动态分配二维数组时,通常会使用二级指针。
- 函数参数传递:当需要在函数内部修改一级指针的值时,需要使用二级指针作为函数参数。
示例代码
1. 访问变量的值
#include <stdio.h>
int main() {
int num = 10;
int *pa = &a;
int **ppa = &pa;//存储的是一级指针的地址
printf("a的值: %d\n", a);
printf("通过一级指针访问a的值: %d\n", *pa);
printf("通过二级指针访问a的值: %d\n", **ppa);
return 0;
}
代码解释:
*pa
是通过一级指针访问a
的值。**ppa
是先通过二级指针ppa
找到一级指针pa
,再通过pa
找到a
的值。
2. 动态分配二维数组(这里看不懂可以跳过,等学习了动态内存分配再来看哦)
#include <stdio.h>
#include <stdlib.h>
int main()
{
int rows = 3;
int cols = 4;
// 动态分配二维数组
int **matrix = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++)
{
matrix[i] = (int *)malloc(cols * sizeof(int));//申请动态内存
}
// 初始化二维数组
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++)
{
matrix[i][j] = i * cols + j;
}
}
// 打印二维数组
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
printf("%d ", matrix[i][j]);
}
printf("\n");
}
// 释放内存
for (int i = 0; i < rows; i++)
{
free(matrix[i]);
}
free(matrix);//释放内存
return 0;
}
代码解释:
- 首先使用
malloc
函数为二级指针matrix
分配rows
个int *
类型的内存空间。 - 然后为每一行分配
cols
个int
类型的内存空间。 - 初始化并打印二维数组后,需要释放分配的内存,避免内存泄漏。
3. 函数参数传递(动态内存申请看不懂可以先跳过,主要目的让你们知道有这些就已经足够了)
#include <stdio.h>
#include <stdlib.h>
// 函数用于动态分配内存并修改一级指针的值
void allocateMemory(int **ptr) //必须得用二维数组接收
{
*ptr = (int *)malloc(sizeof(int));//申请动态内存
**ptr = 100;
}
int main()
{
int *ptr = NULL;
allocateMemory(&ptr);//一级指针传地址
if (ptr != NULL)
{
printf("分配的内存中的值: %d\n", *ptr);
free(ptr);//释放内存
}
return 0;
}
代码解释:
allocateMemory
函数接受一个二级指针ptr
作为参数,在函数内部为一级指针分配内存并赋值。- 在
main
函数中,将一级指针ptr
的地址传递给allocateMemory
函数,从而在函数内部修改了ptr
的值。
注意事项
- 内存管理:使用二级指针进行动态内存分配时,要确保在使用完后及时释放内存,避免内存泄漏。
- 空指针检查:在使用指针之前,要确保指针不为空,避免空指针引用错误。
5. 指针数组
指针数组
是指针? 还是数组?
指针数组 - 存放指针的数组,数组的每个元素其实是指针类型
char* arr[10];存放字符指针的数组
int* arr[5];存放整型指针的数组
char arr[ 5 ]; 字符数组 - 存放字符的数组
int arr[ 5 ]; 整型数组 - 存放整型的数组
int* arr[5];存放整型指针的数组
使用场景
- 处理多个字符串:在 C 语言中,字符串通常用字符指针表示,指针数组可以方便地存储多个字符串。
- 动态内存管理:当需要管理多个动态分配的内存块时,可以使用指针数组来存储这些内存块的地址。
初始化
指针数组可以在定义时进行初始化,也可以在后续的代码中逐个赋值。
定义时初始化
int main()
{
int a = 10;
int b = 20;
int c = 30;
//如果我们按照以下操作非常繁琐麻烦
/* int* pa = &a;
int* pb = &b;
int* pc = &c; */
int* arr[] = { &a ,&b,&c };//所以我们可以创建一个指针数组(必须类型都是一样的才可以存放在一起)
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d\n", *(arr[i])); // *(*arr+i)
}
return 0;
}
也可以后续逐个赋值
#include <stdio.h>
int main()
{
int a = 10, b = 20, c = 30;
int *arr[3];
arr[0] = &a;
arr[1] = &b;
arr[2] = &c;
for (int i = 0; i < 3; i++)
{
printf("%d ", *arr[i]);
}
printf("\n");
return 0;
}
1. 处理多个字符串
#include <stdio.h>
int main()
{
// 定义一个指针数组来存储多个字符串
char *arr[] = {"Hello","World","C Programming"};
int size = sizeof(arr) / sizeof(arr[0]);//
for (int i = 0; i < size; i++)
{
printf("%s\n", arr[i]);
}
return 0;
}
代码解释:
char *arr[]
定义了一个指针数组,每个元素都是一个指向字符的指针,也就是一个字符串。- 通过
sizeof(arr) / sizeof(arr[0])
计算数组的大小。 - 使用
for
循环遍历指针数组,并打印每个字符串。
2. 动态内存管理(这里看不懂可以跳过,等学习了动态内存分配再来看哦)
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *arr[3];
// 为每个指针分配内存
for (int i = 0; i < 3; i++)
{
arr[i] = (int *)malloc(sizeof(int));//申请动态内存
if (arr[i] == NULL) //判断
{
printf("内存分配失败\n");
return 1;
}
*arr[i] = i * 10;
}
// 打印每个指针指向的值
for (int i = 0; i < 3; i++)
{
printf("%d ", *arr[i]);
}
printf("\n");
// 释放内存
for (int i = 0; i < 3; i++)
{
free(arr[i]);
}
return 0;
}
代码解释:
- 定义了一个包含 3 个元素的指针数组
arr
。 - 使用
malloc
函数为每个指针分配内存,并赋值。 - 打印每个指针指向的值。
- 使用
free
函数释放分配的内存,避免内存泄漏。
注意事项
- 内存管理:如果指针数组中的指针指向动态分配的内存,在使用完后要及时释放内存,避免内存泄漏。
- 空指针检查:在使用指针之前,要确保指针不为空,避免空指针引用错误。
6. 指针数组模拟⼆维数组
#include <stdio.h>
int main()
{
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
//数组名是数组⾸元素的地址,类型是int*的,就可以存放在arr数组中
int* arr[3] = {arr1, arr2, arr3};
//我们可以把
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<5; j++)
{
printf("%d ", arr[i][j]); // *(arr[i]+j) == *(*(arr+i)+j)
//我们可以把*(arr[0]+0)当作是arr1,*(arr[1]+0)当作是arr2,*(arr[2]+0)当作是arr3
}
printf("\n");
}
return 0;
}
总结
通过使用指针数组 arr
存储多个一维数组的首地址,代码实现了对多个一维数组元素的统一管理和遍历输出,模拟了二维数组的访问方式。这种方式在处理多个相关的一维数组时非常方便,但需要注意的是,这里的指针数组所模拟的二维数组在内存中并不是连续存储的,与真正的二维数组有所区别。
7. 字符指针变量
定义和初始化
定义
字符指针变量用于存储字符或字符串的地址。其定义形式为:
char *指针变量名;
例如:
char *arr;
这里定义了一个名为arr
的字符指针变量。
初始化
- 指向单个字符
char ch = 'A';
char *arr = &ch;
在这个例子中,arr
指针变量存储了字符ch
的地址。
- 指向字符串常量
char *arr = "Hello";
这里arr
指向了字符串常量"Hello"
的首字符地址。需要注意的是,字符串常量存储在只读内存区域,不能通过该指针修改字符串的内容。
使用场景
1. 字符串处理
字符指针常用于处理字符串,例如字符串的复制、比较、查找等操作。
#include <stdio.h>
int main()
{
char *arr = "Hello, World!";
printf("%s\n", arr); // 输出字符串
// 遍历字符串
while (*arr != '\0')
{
printf("%c ", *arr);
arr++;
}
printf("\n");
return 0;
}
在上述代码中,arr
指针指向字符串常量"Hello, World!"
,可以直接使用printf
函数输出该字符串。同时,通过while
循环遍历字符串的每个字符,每次访问完一个字符后,指针arr
向后移动一位。
2. 函数参数传递
在函数调用时,可以使用字符指针作为参数传递字符串,避免了整个字符串的复制,提高了效率。
#include <stdio.h>
// 函数用于计算字符串的长度
int stringLength(char *arr)
{
int len = 0;
while (*arr != '\0')
{
len++;
arr++;
}
return len;
}
int main()
{
char *arr = "Hello";
int length = stringLength(arr);
printf("字符串的长度是: %d\n", length);
return 0;
}
在这个例子中,stringLength
函数接受一个字符指针作为参数,通过遍历指针所指向的字符串,计算字符串的长度。
注意事项
1. 字符串常量的只读性
当字符指针指向字符串常量时,不能修改该字符串的内容。例如:
char *arr = "Hello";
// str[0] = 'J'; // 错误,不能修改字符串常量
如果需要修改字符串的内容,应该使用字符数组来存储字符串。
2. 空指针检查
在使用字符指针之前,应该确保指针不为空,避免空指针引用错误。例如:
char *str = NULL;
if (str != NULL) {
// 进行操作
}
3. 内存管理
如果字符指针指向动态分配的内存,使用完后要及时释放内存,避免内存泄漏。例如:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char *arr = (char *)malloc(10 * sizeof(char));//申请动态内存
if (arr != NULL)
{
strcpy(arr, "Hello");
printf("%s\n", arr);
free(arr); // 释放内存
}
return 0;
}
在这个例子中,使用malloc
函数动态分配了一块内存,使用完后使用free
函数释放该内存。
这里有一个例子:
int main()
{
char str1[] = "hello World";
char str2[] = "hello World";
const char* str3 = "hello World";
const char* str4 = "hello World";
if (str1 == str2)
printf("str1 and str2 are same\n");//1
else
printf("str1 and str2 are not same\n");//2
if (str3 == str4)
printf("str3 and str4 are same\n");//3
else
printf("str3 and str4 are not same\n");//4
return 0;
}
大家觉得会打印 1 2 3 4 那几个呐?
这里我们来分析一下把
1. str1
和 str2
的比较
char str1[] = "hello World";
char str2[] = "hello World";
- 内存分配:
str1
和str2
都是字符数组。当你使用字符数组来存储字符串时,编译器会为每个数组在栈上分配独立的内存空间,然后将字符串常量的内容复制到这些内存空间中。也就是说,str1
和str2
是两个不同的数组,它们在内存中有各自独立的存储区域。 - 比较过程:在
if(str1 == str2)
中,str1
和str2
作为数组名,在这里会隐式转换为指向数组首元素的指针。由于str1
和str2
是不同的数组,它们的首元素地址是不同的,所以str1 == str2
的比较结果为false
,因此输出str1 and str2 are not same
。
2. str3
和 str4
的比较
const char *str3 = "hello World";
const char *str4 = "hello World";
- 内存分配:
str3
和str4
是指向字符串常量的指针。字符串常量"
hello World"
存储在内存的只读数据段(常量区)。为了节省内存,编译器通常会将相同的字符串常量合并,即只存储一份字符串常量,多个指向相同字符串常量的指针实际上指向的是内存中同一个地址。 - 比较过程:在
if(str3 == str4)
中,str3
和str4
是指针,它们存储的是字符串常量"
hello World"
的首字符地址。由于编译器对相同字符串常量进行了合并,str3
和str4
指向的是同一个内存地址,所以str3 == str4
的比较结果为true
,因此输出str3 and str4 are same
。
总结
- 字符数组在栈上有独立的内存空间,即使内容相同,它们的地址也不同。
- 字符串常量存储在只读数据段,相同的字符串常量通常只存储一份,多个指向相同字符串常量的指针指向同一个地址。
- 在 C 语言中,使用
==
比较指针时,比较的是指针存储的地址,而不是指针所指向的内容。如果需要比较字符串的内容,可以使用strcmp
函数。
8. 数组指针变量
1. 数组指针变量的定义
数组指针变量的定义格式为:数据类型 (*指针变量名)[数组元素个数];
。其中,数据类型
表示数组元素的类型,指针变量名
是自定义的变量名,数组元素个数
明确了该指针所指向数组的元素数量。
示例代码如下:
#include <stdio.h>
int main() {
// 定义一个指向包含 5 个整型元素的数组的指针
int ( *p )[ 5 ];
return 0;
}
在上述代码中,p
就是一个数组指针变量,它指向的数组包含 5 个 int
类型的元素。
这里我们也可以类比一下
字符指针 - char* - 指向字符的指针 - 字符指针变量中存放字符变量的地址
char ch = ‘w';
char* pc = &ch;
整型指针 - int* - 指向整型的指针 - 整型指针变量中存放整型变量的地址
int a = 10;
int* p = &a;
数组指针 - 指向数组的指针 - 数组指针变量中存放数组的地址int main()
{
int arr[ 10] = { 1,2,3,4,5 };
int ( *p )[10 ] = &arr; // p就是数组指针,p中存放的是数组的地址
// int( * )[ 10 ] = int( * )[ 10 ]// arr -- int* arr+1 跳过4个字节
// &arr[ 10 ] -- int* &arr[0]+1 跳过4个字节
// &arr -- int( * )[ 10 ] &arr+1 跳过40个字节
int* ptr;
return 0;
}
2. 数组指针变量的初始化
数组指针变量可以指向一个已存在的数组。示例如下:
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
// 定义数组指针变量并初始化为指向数组 arr
int (*p)[5] = &arr;
return 0;
}
这里需要注意的是,要使用 &arr
来获取数组 arr
的地址并赋值给数组指针 p
,而不是直接用 arr
。因为 arr
通常会隐式转换为指向数组首元素的指针,而 &arr
才是整个数组的地址。
3. 使用数组指针变量访问数组元素
通过数组指针变量可以访问其所指向数组的元素。示例代码如下:
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr;
// 访问数组元素
for (int i = 0; i < 5; i++)
{
// 先对数组指针解引用得到数组,再通过下标访问元素
printf("%d ", (*p)[i]);
}
printf("\n");
return 0;
}
在上述代码中,(*p)
表示对数组指针 p
进行解引用操作,得到其所指向的数组,然后通过 [i]
来访问数组中的第 i
个元素。
4. 数组指针变量在二维数组中的应用
数组指针变量常用于处理二维数组。因为二维数组在内存中是按行存储的,每一行可以看作一个一维数组,数组指针可以方便地按行访问二维数组。示例代码如下:
#include <stdio.h>
int main()
{
int arr[3][4] = {
{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};
// 定义数组指针变量,指向包含 4 个整型元素的数组
int (*p)[4] = arr;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
// 通过数组指针访问二维数组元素
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
return 0;
}
在这个例子中,p
是一个指向包含 4 个 int
类型元素的数组的指针。p + i
表示指向第 i
行的数组,*(p + i)
得到第 i
行数组的首地址,*(p + i) + j
指向第 i
行第 j
列元素的地址,*(*(p + i) + j)
则获取该元素的值。
5. 注意事项
- 数组指针变量指向的数组元素个数必须与定义时指定的元素个数一致,否则可能会导致访问越界等问题。
- 区分数组指针和指针数组。指针数组是一个数组,其元素是指针;而数组指针是一个指针,它指向一个数组。例如:
// 指针数组:包含 3 个整型指针的数组
int *ptrArr[3];
// 数组指针:指向包含 3 个整型元素的数组
int (*arrPtr)[3];
如果大家还是对于数组指针和指针数组不知道有什么区别可以看我的博客另外一篇:C语言:数组指针 和 指针数组 的对比-CSDN博客
9. ⼆维数组传参的本质
1. 二维数组在内存中的存储
二维数组在内存中是按行优先顺序连续存储的。例如,对于一个 int arr[3][4]
的二维数组,它在内存中会依次存储第一行的 4 个元素,接着是第二行的 4 个元素,最后是第三行的 4 个元素,形成一个线性的存储序列。
2. 二维数组传参的常见形式及本质
形式一:指定列数的二维数组形式
#include <stdio.h>
// 函数以指定列数的二维数组形式接收参数
void printArray(int arr[][4], int rows)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][4] = {
{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};
printArray(arr, 3);
return 0;
}
- 本质:在这种形式中,虽然函数参数看起来是二维数组,但实际上
arr
会被当作指针来处理。这里arr
是一个指向包含 4 个int
类型元素的数组的指针,也就是数组指针。它接收的是二维数组首行的地址,即&arr[0]
。在函数内部,arr[i][j]
会被转化为*(*(arr + i) + j)
这样的指针运算来访问元素。
形式二:数组指针形式
#include <stdio.h>
// 函数以数组指针形式接收参数
void printArray(int (*arr)[4], int rows)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", *(*(arr + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][4] = {
{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};
printArray(arr, 3);
return 0;
}
- 本质:这种形式与第一种本质相同,
int (*arr)[4]
明确表明arr
是一个数组指针,指向包含 4 个int
类型元素的数组。在调用printArray
函数时,传递的arr
会隐式转换为二维数组首行的地址,即&arr[0]
。函数内部通过指针运算*(arr + i)
得到第i
行的首地址,再通过*(arr + i) + j
得到第i
行第j
列元素的地址,最后通过*(*(arr + i) + j)
访问该元素的值。
3. 传参时列数必须指定的原因
在二维数组传参时,必须指定列数。这是因为在进行指针运算时,需要知道每一行元素的个数,才能正确地定位到相应的元素。例如,arr + i
这个运算,编译器要根据列数来计算偏移量,偏移的字节数是 i * 列数 * sizeof(元素类型)
。如果不指定列数,编译器就无法确定偏移量,从而无法正确访问元素。
4. 不能传递整个二维数组
和一维数组一样,在 C 语言中不能直接将整个二维数组传递给函数。传递的只是二维数组首行的地址,函数并不知道整个二维数组的行数和列数(除了指定的列数),所以通常需要额外传递行数作为参数。
综上所述,C 语言二维数组传参的本质是传递二维数组首行的地址,函数将其当作数组指针来处理,并且传参时必须指定列数以保证正确的指针运算。
10. 函数指针变量
函数指针变量的创建步骤与语法
步骤分析
- 明确函数类型:确定你要指向的函数的返回值类型和参数列表。
- 定义函数指针类型:根据函数的返回值类型和参数列表来定义函数指针的类型。
- 创建函数指针变量:使用定义好的函数指针类型来创建具体的函数指针变量。
语法形式
函数指针变量的一般定义语法如下:
返回值类型 (*指针变量名)(参数类型列表);
其中:
返回值类型
:与要指向的函数的返回值类型相同。指针变量名
:自定义的函数指针变量名称。参数类型列表
:与要指向的函数的参数类型及顺序一致。
函数知否有地址?
int Add(int x, int y)
{
return x + y;
}
int main()
{
//虽然arr和&arr是有区别
// 但‘&函数名’和‘函数名’都是函数的地址,没有区别
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
int Add(int x, int y) 函数指针有几种写法?
{
return x + y;
}
int main()
{// int (*pf)(int, int) = Add;
int (*pf)(int, int) = &Add; //pf 函数指针变量 int (*pf)(int x, int y) = &Add;名字可以不写
// int (*)(int, int) 去掉函数名就是函数指针类型
int ret = (*pf)(4, 5);//int ret = pf(4, 5); *其实不写也是可以的,因为没有实际的用途
printf("%d\n", ret);int ret2 = Add(4, 5);
printf("%d", ret2);
return 0;
}
示例代码及详细解释
示例 1:指向无参数、无返回值的函数
#include <stdio.h>
// 定义一个无参数、无返回值的函数
void hello()
{
printf("Hello, World!\n");
}
int main()
{
// 创建一个函数指针变量,指向返回值为void、无参数的函数
void (*funcPtr)(); //
// 让函数指针变量指向hello函数
funcPtr = hello;
// 通过函数指针调用函数
funcPtr();
return 0;
}
解释:
void (*funcPtr)();
:定义了一个函数指针变量funcPtr
,它可以指向返回值为void
且无参数的函数。funcPtr = hello;
:将函数指针变量funcPtr
指向hello
函数。注意,函数名在这种情况下会隐式转换为函数的地址。funcPtr();
:通过函数指针变量funcPtr
调用其所指向的函数,这里实际上调用的就是hello
函数。
示例 2:指向有参数、有返回值的函数
#include <stdio.h>
// 定义一个有参数、有返回值的函数
int add(int a, int b)
{
return a + b;
}
int main()
{
// 创建一个函数指针变量,指向返回值为int、有两个int型参数的函数
int (*addPtr)(int, int);
// 让函数指针变量指向add函数
addPtr = add;
// 通过函数指针调用函数并接收返回值
int result = addPtr(3, 5);
printf("3 + 5 = %d\n", result);
return 0;
}
解释:
int (*addPtr)(int, int);
:定义了一个函数指针变量addPtr
,它可以指向返回值为int
且有两个int
型参数的函数。addPtr = add;
:将函数指针变量addPtr
指向add
函数。int result = addPtr(3, 5);
:通过函数指针变量addPtr
调用add
函数,并将返回值存储在result
变量中。
注意事项
- 类型匹配:函数指针变量的返回值类型和参数列表必须与要指向的函数完全匹配,否则会导致编译错误。
- 括号的使用:函数指针定义中的括号
(*指针变量名)
不能省略,否则会变成返回指针的函数的声明。例如,int *funcPtr(int, int);
声明的是一个返回int
型指针的函数,而不是函数指针变量。
11. typedef 关键字
基本语法
typedef
的基本语法格式如下:
typedef 原数据类型 新类型名;
其中,原数据类型
可以是 C 语言中任何合法的数据类型,包括基本数据类型(如 int
、char
等)、自定义数据类型(如结构体、联合体等)以及指针类型等;新类型名
是你为该数据类型定义的别名。
具体应用场景及示例
1. 为基本数据类型定义别名
#include <stdio.h>
// 为int类型定义别名Integer
typedef int Integer;
int main()
{
// 使用别名定义变量
Integer num = 10;
printf("num的值: %d\n", num);
return 0;
}
在这个例子中,typedef int Integer;
为 int
类型定义了一个新的名称 Integer
,之后就可以使用 Integer
来定义 int
类型的变量。
2. 为结构体类型定义别名
#include <stdio.h>
// 定义结构体并使用typedef为其定义别名
typedef struct
{
char name[20];
int age;
} Person;
int main()
{
// 使用别名定义结构体变量
Person p = {"John", 25};
printf("姓名: %s, 年龄: %d\n", p.name, p.age);
return 0;
}
这里 typedef struct {...} Person;
为一个匿名结构体定义了别名 Person
,后续就可以直接使用 Person
来定义该结构体类型的变量,而不需要每次都写完整的 struct
定义。
3. 为指针类型定义别名
#include <stdio.h>
// 为int*类型定义别名IntPtr
typedef int* IntPtr;
int main()
{
int num = 10;
// 使用别名定义指针变量
IntPtr ptr = #
printf("ptr指向的值: %d\n", *ptr);
return 0;
}
typedef int* IntPtr;
为 int*
类型定义了别名 IntPtr
,这样就可以用 IntPtr
来定义 int
类型的指针变量。
4. 为函数指针类型定义别名
#include <stdio.h>
// 定义一个函数
int add(int a, int b)
{
return a + b;
}
// 为函数指针类型定义别名
typedef int (*AddFunction)(int, int);
int main()
{
// 使用别名定义函数指针变量
AddFunction func = add;
int result = func(3, 5);
printf("3 + 5 = %d\n", result);
return 0;
}
typedef int (*AddFunction)(int, int);
为返回值为 int
且有两个 int
类型参数的函数指针类型定义了别名 AddFunction
,之后就可以用 AddFunction
来定义函数指针变量。
注意事项
typedef
与#define
的区别:虽然#define
也可以用于定义别名,但typedef
是由编译器处理的,它创建的是真正的类型别名,而#define
是预处理器指令,只是简单的文本替换。例如:#define PINT int* typedef int* IntPtr; PINT a, b; // 这里a是int*类型,b是int类型 IntPtr c, d; // 这里c和d都是int*类型
- 作用域:
typedef
定义的别名的作用域遵循 C 语言的作用域规则。如果在函数内部定义,其作用域仅限于该函数;如果在文件全局作用域定义,则在整个文件中都可以使用
12. 函数指针数组
1. 函数指针数组的定义
函数指针数组的定义需要结合函数指针和数组的定义规则。其一般形式为:
返回值类型 (*数组名[数组大小])(参数类型列表);
其中:
返回值类型
:是数组中每个函数指针所指向函数的返回值类型。数组名
:是自定义的数组名称。数组大小
:指定数组中元素的个数。参数类型列表
:是数组中每个函数指针所指向函数的参数类型及顺序。
2. 函数指针数组的初始化与使用示例
示例 1:简单的函数指针数组
#include <stdio.h>
// 定义几个简单的函数
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 main()
{
// 定义一个函数指针数组,包含3个元素,每个元素指向返回值为int、有两个int型参数的函数
int (*funcArray[3])(int, int) = {add, subtract, multiply};
int num1 = 5, num2 = 3;
// 通过函数指针数组调用不同的函数
for (int i = 0; i < 3; i++)
{
int result = funcArray[i](num1, num2);
switch (i)
{
case 0:
printf("%d + %d = %d\n", num1, num2, result);
break;
case 1:
printf("%d - %d = %d\n", num1, num2, result);
break;
case 2:
printf("%d * %d = %d\n", num1, num2, result);
break;
}
}
return 0;
}
代码解释:
int (*funcArray[3])(int, int) = {add, subtract, multiply};
:定义了一个名为funcArray
的函数指针数组,它包含 3 个元素,每个元素都是一个指向返回值为int
且有两个int
型参数的函数的指针。数组初始化时,分别将add
、subtract
和multiply
函数的地址赋给数组元素。funcArray[i](num1, num2)
:通过循环遍历函数指针数组,调用不同的函数并传入参数num1
和num2
,将结果存储在result
变量中,最后根据i
的值输出相应的运算结果。
示例 2:使用 typedef
简化函数指针数组的定义
#include <stdio.h>
// 定义几个简单的函数
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
// 使用typedef为函数指针类型定义别名
typedef int (*Operation)(int, int);
int main()
{
// 定义一个函数指针数组,使用typedef定义的别名
Operation funcArray[2] = {add, subtract};
int num1 = 10, num2 = 5;
// 通过函数指针数组调用不同的函数
for (int i = 0; i < 2; i++)
{
int result = funcArray[i](num1, num2);
switch (i)
{
case 0:
printf("%d + %d = %d\n", num1, num2, result);
break;
case 1:
printf("%d - %d = %d\n", num1, num2, result);
break;
}
}
return 0;
}
代码解释:
typedef int (*Operation)(int, int);
:使用typedef
为返回值为int
且有两个int
型参数的函数指针类型定义了别名Operation
。Operation funcArray[2] = {add, subtract};
:使用Operation
别名定义了一个函数指针数组funcArray
,使代码更加简洁易读。
3. 函数指针数组的应用场景
函数指针数组常用于实现菜单系统、状态机等。例如,在一个简单的计算器程序中,可以使用函数指针数组根据用户输入的运算符选择不同的运算函数进行计算,避免使用大量的 if-else
或 switch
语句,提高代码的可扩展性。
13.转移表
在 C 语言中,转移表(Jump Table)是一种利用函数指针数组实现的技术,它允许程序根据某个变量的值来快速地跳转到不同的代码段执行,从而避免大量使用 if-else
或 switch
语句,提高代码的执行效率和可维护性。下面详细介绍转移表的原理、实现和应用场景。
原理
转移表的核心思想是使用一个函数指针数组,数组中的每个元素都是一个函数指针,分别指向不同的处理函数。程序通过一个索引值来访问数组中的特定元素,进而调用对应的处理函数。这样,根据不同的输入值,可以快速定位并执行相应的代码,而不需要进行逐个条件的判断。
实现步骤及示例代码
步骤分析
- 定义处理函数:编写一系列处理函数,这些函数具有相同的返回值类型和参数列表。
- 创建函数指针数组:定义一个函数指针数组,数组的每个元素指向一个处理函数。
- 使用索引调用函数:根据输入的索引值,从函数指针数组中选择对应的函数并调用。
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
// 0 1 2 3 4
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf( "请选择:" );
scanf("%d", &input);
if ((input <= 4 && input >= 1)) //输入数字的判断
{
printf( "输⼊操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);//根据相应的数字调用相同的计算
printf( "ret = %d\n", ret);
}
else if(input == 0)
{
printf("退出计算器\n");
}
else
{
printf( "输⼊有误\n" );
}
}while (input);
return 0;
}
代码解释
转移表的优势
- 显示菜单:使用
printf
函数显示操作菜单,提示用户选择运算操作或退出程序。 - 获取用户输入:使用
scanf
函数读取用户输入的选项input
。 - 处理用户选择:
- 如果用户输入的
input
在 1 到 4 之间,表示用户选择了某种运算操作。程序会提示用户输入两个操作数x
和y
,然后通过(*p[input])(x, y)
调用对应的运算函数,将结果存储在ret
中并输出。 - 如果用户输入的
input
为 0,表示用户选择退出程序,程序会输出相应提示信息。 - 如果用户输入的
input
不在有效范围内,程序会输出 “输入有误” 的提示信息。
- 如果用户输入的
- 循环条件:
do - while
循环的条件是input
,只要input
不为 0,循环就会继续执行,直到用户选择退出。 - 代码简洁:避免了使用大量的
if - else
或switch
语句来进行函数选择,使代码结构更加清晰。 - 易于扩展:如果需要添加新的运算操作,只需要定义新的运算函数,并将其函数地址添加到转移表中即可,不需要修改大量的条件判断代码。
应用场景
- 菜单系统:在具有多个选项的菜单系统中,使用转移表可以根据用户的选择快速调用相应的处理函数,避免使用大量的
if-else
或switch
语句,使代码更加简洁和易于维护。 - 状态机:在状态机的实现中,根据当前状态和输入事件,可以使用转移表快速跳转到下一个状态的处理函数,提高状态转换的效率。
注意事项
- 边界检查:在使用转移表时,必须对输入的索引值进行边界检查,确保其在函数指针数组的有效范围内,否则可能会导致未定义行为。
- 函数签名一致性:转移表中的所有函数必须具有相同的返回值类型和参数列表,以确保函数指针数组的正确性。