指针基础
什么是指针,介绍一下你对指针的理解
指针是一种特殊的变量,存储的是内存地址,而不是普通数据的值。
比如int *p;表示p是一个指针,只能存储int类型数据的内存地址。
指针的类型(如int *、char *)决定了他能指向的数据类型,以及通过指针访问内存时的操作长度,比如int *指针每次移动会跨越4字节,对应int 的大小。
内存地址:通过&符号可以获取变量的地址。
int a = 10;
int *p = &a; // p 存储变量 a 的内存地址
可以通过解引用符号 *,来间接访问指针所指向的内存中的值:
int a = 5, *p = &a;
printf("%d", *p); // 输出 5(通过指针 p 访问 a 的值)
*p = 10; // 通过指针修改 a 的值,此时 a 变为 10
指针的算术运算:指针可以进行加减整数操作,结果与指针类型相关:
int a[3] = {1, 2, 3};
int *p = a; // p 指向数组首元素(a[0])的地址
p++; // p 指向 a[1] 的地址(地址增加 sizeof(int), 4 字节)
printf("%d", *p); // 输出 2
如何定义和初始化一个指针变量?
定义:
基本格式为:数据类型 *指针变量名
比如:
int *p; // 定义一个指向 int 类型的指针变量
char *p; // 定义一个指向 char 类型的指针变量
float *p; // 定义一个指向 float 类型的指针变量
初始化指针变量:
需要为其赋一个合法的内存地址,或初始化为NULL空指针。
1.指向普通变量的地址:
int n = 10;
int *p = &n; // 定义时初始化,p 指向变量 n 的地址
2.指向数组元素的地址:
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // 初始化指向数组首元素(等价于 &a[0])
int *p2 = &a[2]; // 初始化指向数组第 3 个元素(值为 3)
3.指向动态分配的内存(使用malloc)
int *p = (int *)malloc(sizeof(int)); // 分配一个 int 类型的内存空间
*p = 100; // 通过指针操作内存空间
什么是地址运算符(&)和解引用运算符(*)?
地址运算符&:
作用是获取一个变量的内存地址:
int a = 10;
int *p = NULL; // 定义一个指向 int 类型的指针变量 p
p = &a; // 将变量 a 的地址赋给指针 p(&a 表示获取 a 的地址)
每个变量在内存中都有一个唯一的地址,&用于获取这个地址。
指针变量(p)的作用是存储其他变量的地址,而&是将变量的地址赋值给指针。
解引用运算符*:
作用是通过指针变量访问它所指向的内存地址中的值:
int a = 10;
int *p = &a; // p 指向 a 的地址
printf("%d\n", *p); // 输出 10(*p 表示访问 p 指向的地址中的值,即变量 a 的值)
*p = 20; // 通过指针修改 a 的值,此时 a 的值变为 20
*用于操作指针所指向的数据,当指针指向一个变量时,*p等价于该变量本身。
*p = 20 等价于 a = 20。
空指针是什么?如何使用?
空指针是一个指针变量,其值为NULL。
1.初始化的时候使用:
int *p = NULL; // 声明时初始化为空指针
是合法的指针值,这样的初始化可以避免成为野指针。
2.函数返回值的错误标识
使用malloc时,内存分配失败时返回NULL,可以用if语句来判断内存是否分配失败。
int *a = (int*)malloc(sizeof(int));
if (arr == NULL)
{
printf("内存分配失败!\n");
}
3.数据结构的结束标志
在链表、树等数据结构中,空指针常用于表示节点的末尾(如链表的尾节点指针指向NULL)
struct Node {
int data;
struct Node *next;
};
struct Node *head = NULL; // 空链表的头指针为 NULL
野指针是什么?如何避免?
野指针是指向非法内存地址或已释放内存空间的指针。
野指针的常见成因:
1.未初始化的指针。
2.指针指向的内存已释放:
int *p = (int *)malloc(sizeof(int));
free(p); // 释放内存
*p = 10; // 此时 ptr 是野指针,解引用非法
3.指针越界访问:
int a[5] = {1, 2, 3, 4, 5};
int *p = a;
p += 5; // 指向数组末尾之后的内存,解引用非法
4.函数返回局部变量的地址:
int *fp()
{
int n = 10; // 局部变量,存储在栈上
return &n; // 返回局部变量地址,函数返回后 num 的内存被释放
}
int *p = fp(); // ptr 是野指针
避免野指针:
1.初始化指针。
2.使用free()释放内存后,立即将指针置为NULL。
3.函数若需返回内存地址,应返回动态分配的内存(如malloc),或指向全局变量的指针。
int *fp()
{
int *p = (int *)malloc(sizeof(int)); // 动态分配内存
*p = 10;
return p; // 合法,调用者需负责释放
}
指针运算
指针可以进行哪些运算?
指针的算术运算主要和指针所指向的数据类型有关,编译器会根据数据类型来计算实际的内存偏移量。
1.指针与整数的加减:
指针 + 整数:指针向前偏移 整数倍的sizeof(数据类型)个字节。
例如,若int *p指向地址0x1000,那么p + 1指向0x1004(int 占四个字节)
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // p 指向 arr[0](地址 0x1000)
p = p + 2; // p 指向 arr[2](地址 0x1008)
指针 - 整数:指针向后偏移整数倍的sizeof(数据类型)个字节。
2.指针与指针的相减:
当两个指针指向同一块连续内存区域(如数组元素)时,相减结果为他们之间的元素个数(而非字节差)。
int *p1 = &a[4]; // 指向 arr[4]
int *p2 = &a[1]; // 指向 arr[1]
int diff = p1 - p2; // diff = 3(4 - 1 = 3 个元素)
3.指针的自增和自减:
前置、后置自增:指针指向下一个元素。
p++; // 等价于 p = p + 1,指向 arr[2]
前置、后置自减:指针指向前一个元素。
p--; // 等价于 p = p - 1,指向 arr[1]
4.指针可以通过关系运算符(< <= > >= == !=)进行比较。
1.指向同一块连续内存区域时(比如数组元素),比较的是指针的地址顺序。
int *p = a, *q = &a[3];
if (p < q) // p 的地址小于 q 的地址,条件成立
{
printf("p在q的前面\n");
}
2.判断指针是否为空
int *p = NULL;
if (p == NULL) // 指针未初始化,避免野指针解引用
{
printf("ptr is null\n");
}
5.指针的间接访问运算 *
通过解引用运算符*来获取指针指向的内存值,或修改值。
int n = 10;
int *p = &n;
printf("%d\n", *p); // 输出 10(访问 x 的值)
*p = 20; // 修改 x 的值为 20
printf("%d\n", n); // 输出 20
6.指针的赋值运算
将一个指针的值(也就是地址),赋给另一个同类型的指针,使他们指向同一内存位置。
int a[5] = {1, 2, 3, 4, 5};
int *p1 = a; // p1 指向 a[0]
int *p2 = p1 + 2; // p2 指向 a[2]
p1 = p2; // p1 现在也指向 a[2]
如何比较两个指针?
比较两个指针的地址值
== :判断两个指针是否指向同一个地址
!= :判断两个指针是否指向不同地址。
> < :当指针指向同一数组元素时,表示两个指针的相对位置。
指针与数组
数组名和指针有什么关系?
1.大部分表达式中,数组的数组名会被自动转换成指向数组首元素的指针。
2.当数组名作为函数参数进行传参时,被视为指针。
3.当用sizeof运算符来获取数组整体大小时,不会被看作指针,而是整个数组。
而如果sizeof指针的话,就是指针变量本身的字节大小。
4.用&来取数组地址时,数组名代表的是整个数组,而不是指针。
如何通过指针访问数组元素?
1.数组名等价于指向首元素a【0】的指针。可以直接解引用 ,用*(a + i)来访问。
*(a + i)完全等价于a【i】。
2.定义一个指针变量p,让它指向数组首元素,然后p++或p--来访问数组元素。
指针数组和数组指针的区别是什么?
指针数组:
本质是一个数组,数组的每个元素都是指针。例如:
int *a[5] 5个元素,每个元素都是int * 的指针。
因为[]的优先级要高于*,所以a会先与[]结合,表明是数组,每个元素都是指针。
数组指针:
本质是一个指针,该指针指向一个数组。例如:
int (*p)[5] p是一个指针,指向包含五个int 类型元素的数组。
()改变了优先级,使p先与*结合了,变成了指针,指向的目标是一个数组。
指针与函数参数
如何将指针作为函数参数?
1.当声明了一个函数,其参数为int类型的指针,
然后我们要在主函数调用它并修改调用者提供的变量时,就需要取地址来传递。
// 函数声明:参数为 int 类型的指针
void fun(int *p)
{
*p = 100; // 通过指针修改指向的值
}
int main()
{
int n = 50;
printf("修改前:%d\n", n); // 输出:50
fun(&n); // 传递 n 的地址(&n)
printf("修改后:%d\n", n); // 输出:100
return 0;
}
2.当处理数组时,则传递数组名,因为数组的数组名就是指向数组首元素的指针。
可以用一个for循环来打印全部的数组元素。
// 函数声明:参数为 int 类型数组的指针(等价于 int a[] 或 int *a)
void fun(int *a, int len)
{
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ", *(a + i)); // 等价于 a[i]
}
}
int main()
{
int a[] = {1, 2, 3, 4, 5};
int len = sizeof(a) / sizeof(a[0]);
fun(a, len); // 传递数组名(即首元素地址)
return 0;
}
如何通过指针修改实参的值?
通过指针修改实参的值需要向函数传递实参的地址(指针),然后在函数内部通过解引用*指针来操作原始变量。
// 定义函数:通过指针修改传入的整数值
void fun(int *p)
{
// 解引用指针并赋值,直接修改原始变量
*p = 100;
}
int main()
{
int n = 10;
printf("修改前:n = %d\n", n); n = 50
// 传递 original 的地址(指针)给函数
fun(&n);
printf("修改后:n = %d\n", n); n = 100
return 0;
}
多级指针
什么是二级指针?请举例说明
二级指针是指向指针的指针,即该指针变量存储的是另一个指针变量的地址。一级指针指向普通变量,而二级指针则指向一级指针。
例如: int **pp 表示pp是一个二级指针,指向一个指向int类型的指针。
int main()
{
int n = 10; // 普通变量,存储整数 10
int *p = &n; // 一级指针 p,存储 num 的地址
int **pp = &p; // 二级指针 pp,存储一级指针 p 的地址
// 访问变量的值
printf("num = %d\n", num); // 直接访问:10
printf("*p = %d\n", *p); // 通过一级指针访问:10
printf("**pp = %d\n", **pp); // 通过二级指针访问:10
// 访问指针的地址
printf("&num = %p\n", &num); // num 的地址
printf("p = %p, &p = %p\n", p, &p); // p 存储的是 num 的地址,&p 是 p 本身的地址
printf("pp = %p, &pp = %p\n", pp, &pp); // pp 存储的是 p 的地址,&pp 是 pp 本身的地址
// 通过二级指针修改变量的值
**pp = 20;
printf("修改后 num = %d\n", num); // 输出:20
return 0;
}
二级指针可以用与动态内存分配中的指针传递:
// 动态分配二维数组(行指针数组)
int r = 3, c = 4;
int **pp = (int **)malloc(r * sizeof(int *));
for (int i = 0; i < r; i++)
{
pp[i] = (int *)malloc(c * sizeof(int));
}
通过二级指针,每一行的长度可以不相同(如锯齿数组)。灵活性更高。
如何定义和使用二级指针?
定义:
定义二级指针时,要使用两个**,比如int **pp
使用:
可以两次解引用,来访问和修改一级指针所指向的变量的值。
在函数传参时,可以让函数修改一级指针的值:
void fun(int **pp)
{
static int n = 30;
*pp = &n;
}
int main()
{
int n = 10;
int *p = # // 一级指针,指向 num
int **pp = &p; // 二级指针,指向一级指针 ptr
printf("修改前 ptr 指向的值: %d\n", *p);
fun(pp);
printf("修改后 ptr 指向的值: %d\n", *p);
return 0;
}
需要注意的是,在 fun
函数中,n
是一个局部变量,当函数执行完毕后,n
的内存会被释放。因此,在 main
函数中输出修改后 ptr
指向的值时,可能会得到一个不确定的值,这是因为 ptr
指向的内存已经被释放。因此需要在前面加上static。使用静态变量,确保其生命周期贯穿整个程序。
函数指针
什么是函数指针?如何定义和使用函数指针?
函数指针:
是一种指向函数的指针变量,它存储了函数在内存中的入口地址。
定义:
基本格式为:返回类型 (*指针变量名)(参数列表)
例如:int (*fun)(int a)
使用:
1.函数指针的赋值:
函数指针需要指向一个具体的函数,直接使用函数名即可获取函数的地址。:
// 假设存在目标函数:int my_fun();
func_ptr = my_fun; // 或 &my_fun(取地址符可选,函数名本身会隐式转换为地址)
2.通过函数指针调用函数:
使用(*指针变量名)(参数):
int result = 0;
result = (*fun)(); // 调用 fun 指向的函数
如何通过函数指针调用函数?
// 定义一个普通函数
int add(int a, int b)
{
return a + b;
}
int main()
{
int result = 0;
// 声明函数指针,其指向的函数返回值为 int,参数为两个 int
int (*fun)(int a, int b);
// 将函数指针指向具体的函数(这里指向 add 函数)
fun = add;
// 通过函数指针调用函数
result = fun(3, 5);
printf("通过函数指针调用的结果:%d\n", result);
return 0;
}
如何定义和使用指向函数的指针数组?
// 定义三个不同的函数,用于后续赋值给函数指针
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 main()
{
int x = 10, y = 5;
// 定义指向函数的指针数组,每个元素指向返回int、参数为两个int的函数
int (*fun[3])(int a, int b);
// 将函数地址赋值给数组元素(函数名即为函数地址)
fun[0] = add;
fun[1] = sub;
fun[2] = mul;
// 通过函数指针数组调用函数
printf("add: %d\n", func[0](x, y)); // 调用 add 函数
printf("subtract: %d\n", func[1](x, y)); // 调用 sub 函数
printf("multiply: %d\n", func[2](x, y)); // 调用 mul 函数
return 0;
}
这种指针数组可以通过修改数组元素就切换不同的函数功能,提高代码拓展性。
回调函数
什么是回调函数?回调函数的应用场景有哪些?
定义:
简单来说,回调函数是"被其他函数调用的函数",当某段代码需要在特定事件发生时、或完成某个操作后调用一个自定义函数时,会将这个自定义函数的指针作为参数传递进去,供其在适当的时候调用。
回调函数本质是函数指针。
应用场景:
// 回调函数:定义 int 类型数组的升序比较规则
int compare(const void* a, const void* b)
{
return (*(int*)a - *(int*)b);
}
int main()
{
int arr[] = {3, 1, 2};
qsort(arr, 3, sizeof(int), compare); // 传递回调函数
return 0;
}
比如C语言标准库里的qsort函数是一个通用排序函数,它不知道我要排序的数据是什么类型(整数、结构体、字符串等),也不知道我希望按什么规则去进行排序(升序、降序、自定义逻辑),因此。qsort函数需要我通过回调函数告诉它两件事:
1.排序顺序是什么?
2.数据类型的大小?
那么就可以写一个compare函数,这个就是传递给qsort的回调函数。
函数的参数选择const void* 。这是因为:
void * 是通用指针,可以接收任何数据类型(因为qsort是通用函数,可能有很多种不同的数据类型)
const表示不修改传入的数据,遵循只读的规则。
当qsort比较两个元素时,会把这两个元素的地址强制转换为const void*传递给compare。
然后在回调函数里写排序逻辑,比如:
return (*(int*)a - *(int*)b);
表示当 a
的值(如 3)大于 b
的值(如 1)时,返回 3-1=2
(正数),表示 a
应在 b
后面。
同样的,当 a
的值(如 1)小于 b
的值(如 2)时,返回 1-2=-1
(负数),表示 a
应在 b
前面。
整个代码执行流的主动权在qsort手上,qsort来决定什么时候调用compare回调函数。
compare告诉qsort"怎么比",而qsort负责"怎么排"。
指针数组与数组指针
什么是指针数组?如何定义和使用指针数组?
定义:
指针数组是一个数组,其每一个元素都是指针。基本格式为:
数据类型 *数组名[数组长度] 比如int *a[3]
使用:
1.指向整数的指针数组:
int n1 = 10, n2 = 20, n3 = 30;
int *p[3]; // 定义一个包含 3 个 int 类型指针的数组
// 初始化指针数组,使其指向不同的整数
p[0] = &n1;
p[1] = &n2;
p[2] = &n3;
2.指向字符的指针数组(常用与字符串):
char *p[3] =
{
"Hello", // 指针指向字符串常量 "Hello" 的首地址
"World", // 指针指向字符串常量 "World" 的首地址
"C Language"
};
3.通过数组下标访问指针数组的元素,再通过解引用*来获取指向的值:
// 示例 1 中访问整数的值
printf("Value of num1: %d\n", *p[0]); // 输出 10
// 示例 2 中访问字符串
printf("First string: %s\n", p[0]); // 输出 Hello
注意到:
示例1中,p[0]的类型是int *(指向int 的指针),需求是获取指针指向的整数的值(即n1的值10)。而解引用的作用,*p[0] 表示"访问 p[0] 所指向的内存地址的值",printf 中的%d 需要一个int 类型的参数,而p[0]是int * 类型,因此需要解引用。
示例2 中,p[0]的类型是char * (指向char 的指针),但这里它指向字符串"Hello"的首字符"H"的地址,而字符串本质上是字符数组,由char 类型的连续内存空间组成,以 "\0" 结尾。
printf的 %s 格式符要求传入一个char * 类型的参数(指向字符串首字符的指针),它会自动从该指针开始读取字符,直到遇到 '\0' ,
示例2的 p[0] 本身就是char *类型,满足%s 的参数要求,因此无需解引用。
如果加上解引用,则会得到首字符 'H' ,后续字符会读不到。
4.作为函数参数
void fun(char *a[], int len)
{
int i = 0;
for (i = 0; i < len; i++)
{
printf("%s\n", a[i]);
}
}
// 调用函数
fun(p, 3);
5.动态内存分配
int n = 5;
int i = 0;
int *a[n]; // 定义指针数组
for (i = 0; i < n; i++)
{
a[i] = (int *)malloc(sizeof(int)); // 为每个指针分配内存
*a[i] = i + 1; // 赋值
}
// 访问数据
printf("Third value: %d\n", *a[2]); // 输出 3
// 释放内存
for (i = 0; i < n; i++)
{
free(a[i]);
}
什么是数组指针?如何定义和使用数组指针?
定义:
数组指针是一个指向数组的指针变量,存储的是整个数组的起始地址。
数组指针的类型需要同时指定数组元素的类型和数组的长度,本质是指向一个特定长度数组的指针。
基本格式为: 数据类型 (*指针变量名)[数组长度]
例如:
int a[5] = {1, 2, 3, 4, 5}; // 定义一个 int 类型的数组
int (*p)[5]; // 定义数组指针,指向长度为 5 的 int 数组
初始化:
将数组指针指向同一类型、同长度的数组,直接使用数组名(数组名表示数组首地址):
ptr = &a; // 或直接写 ptr = arr;(因为数组名隐式转换为指向数组首元素的指针,但数组指针需要指向整个数组,此处 &a 更明确)
使用:
通过数组指针访问元素时,需要先解引用指针得到数组,在通过下标索引访问元素:
printf("a[2] = %d\n", (*p)[2]); // 等价于 a[2],输出 3
指针函数与函数指针
什么是指针函数?如何定义和使用指针函数?
定义:
指针函数是一种返回值为指针(即内存地址)的函数,本质是函数,只是返回的类型是指针。
使用:
1.返回字符串指针的函数:
// 指针函数:返回一个指向 char 类型的指针(字符串)
char *fun()
{
// 动态分配内存(需手动释放)
char *str = (char *)malloc(100 * sizeof(char));
strcpy(str, "Hello, World!");
return str; // 返回动态分配的内存地址
}
int main()
{
char *p = fun(); // 调用指针函数
printf("%s\n", p); // 输出字符串
free(p); // 释放内存,避免内存泄漏
return 0;
}
2.返回指向数组元素的指针:
// 指针函数:返回指向 int 数组中最大值元素的指针
int *fun(int a[], int size)
{
int *max = &a[0]; // 初始假设第一个元素是最大值
int i = 0;
for (i = 1; i < size; i++)
{
if (a[i] > *m)
{
max = &a[i]; // 更新最大值指针
}
}
return max; // 返回最大值元素的地址
}
int main()
{
int n[] = {10, 20, 30, 15, 25};
int *max = fun(n, 5);
printf("最大值:%d\n", *max); // 输出 30
return 0;
}
指针函数和函数指针的区别是什么?
|-----|------------------|---------------------|
| 特征 | 指针函数 | 函数指针 |
| 核心 | 函数,返回指针 | 指针,指向函数 |
| 声明 | int *a() | int (*a)() |
| 用途 | 返回内存地址(动态分配内存) | 存储函数地址(回调函数) |
| 优先级 | 函数名优先级高于*,先调用函数 | ()优先级高于*,需括起来明确是指针 |