系列文章目录
文章目录
1. 指针
指针是存储内存地址的变量,该地址指向存储在计算机内存中的某个位置的数据。指针是内存中一个最小单元的编号,也就是地址。平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
2. 指针变量
要声明一个指针变量,需要在变量类型前加上星号(*
),这表明这是一个指向该类型数据的指针。指针的类型决定了指针操作的步幅(即指针增加或减少时应移动的内存字节数)和能够访问的数据类型。
-
指针变量的初始化
指针变量通常初始化为某个变量的地址,使用地址运算符 &
获取一个变量的地址。
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。
return 0;
}
3. 指针类型
指针有以下类型:
char *pc = NULL; //char* 类型的指针是为了存放 char 类型变量的地址。
int *pi = NULL; //int* 类型的指针是为了存放 int 类型变量的地址。
short *ps = NULL; //short* 类型的指针是为了存放 short 类型变量的地址。
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
3.1 指针+-整数
int main()
{
int n = 10; // 声明一个整数变量 n 并初始化为 10
char *pc = (char*)&n; //声明一个字符指针 pc 并初始化为指向n的地址,但将其类型转换为 char*
int *pi = &n; // 声明一个整数指针 pi 并初始化为指向 n 的地址
printf("%p\n", &n); // 打印变量 n 的地址
printf("%p\n", pc); // 打印字符指针 pc 指向的地址
printf("%p\n", pc+1); // 打印 pc+1,即 pc 向前移动一个字符的长度(1字节)
printf("%p\n", pi); // 打印整数指针 pi 指向的地址
printf("%p\n", pi+1); // 打印 pi+1,即 pi 向前移动一个整数的长度(4字节)
return 0;
}
这段 C 语言代码演示了如何对不同类型的指针执行加法操作,并展示了指针加法如何依赖于指针所指向的数据类型的大小。
3.2 指针的解引用
假设你有一个整数变量和一个指向这个整数的指针,你可以使用解引用操作符来读取和修改这个整数的值:
#include <stdio.h>
int main() {
int value = 10;
int *pointer = &value; // `pointer` 现在指向 `value` 的地址
// 使用解引用操作符来获取指针指向的值
printf("The value pointed to by pointer is: %d\n", *pointer);
// 修改指针指向的内存中存储的值
*pointer = 20; // 改变 `value` 的值
printf("The new value pointed to by pointer is: %d\n", *pointer);
return 0;
}
//The value pointed to by pointer is: 10
//The new value pointed to by pointer is: 20
在这个例子中,pointer 是一个指向 value 的指针。通过使用解引用操作符 *pointer,你首先获取了 value 的原始值(10),然后修改了 value 的值(改为 20)。
4. 野指针
野指针是指向未知或无效内存区域的指针。野指针的存在通常是由于不当的内存管理和指针操作引起的,它们会导致程序行为不可预测,甚至导致程序崩溃和数据损坏。
4.1 野指针形成的原因
未初始化的指针: 未经初始化直接使用的指针。因为它们没有被明确地赋予任何地址,它们的值是不确定的,可能指向任何内存区域。
int *ptr; // 声明了一个整型指针,但未初始化
*ptr = 10; // 尝试赋值操作,但因为 ptr 指向的是随机位置,这可能导致崩溃
已释放的内存: 指向已经释放的内存的指针,通常称为悬挂指针。在释放动态分配的内存后,原指向该内存的指针仍然保留内存的地址,但该地址已不再有效。
int *ptr = malloc(sizeof(int)); // 动态分配内存
*ptr = 5; // 使用分配的内存
free(ptr); // 释放内存
*ptr = 10; // 再次使用这个指针,这是非法操作
指针运算越界: 如果通过指针运算(如增加或减少指针),使得指针超出其原始指向的数据结构的边界,那么这个指针就可能变成野指针。这种越界的指针可能指向任意的内存区域,其行为是未定义的。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr + 5; // 指向数组末尾之后的位置
*ptr = 10; // 越界操作,野指针解引用
4.2 避免和处理野指针
初始化所有指针: 在声明指针时,最安全的做法是立即将其初始化为 NULL
或一个合法的、明确的地址。
int *ptr = NULL; // 初始化为 NULL,安全的做法
释放后置空: 在使用 free()
释放指针指向的内存后,立即将指针设置为 NULL
。这样可以防止悬挂指针的问题。
free(ptr);
ptr = NULL; // 避免悬挂指针
小心处理指针运算: 在进行指针运算时,务必确保不会越界,并且操作符合逻辑。对于数组操作,确保索引值在有效范围内。
int arr[5];
for (int i = 0; i < 5; i++) {
arr[i] = i; // 确保索引不会越界
}
4.3 代码示例
假设我们有一个整数数组和一个指针,我们想通过指针遍历数组并进行某些操作。在这个过程中,非常重要的是要确保指针不会指向数组的边界之外,这样可以避免野指针和潜在的内存错误。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 指针初始化,指向数组的第一个元素
// 通过指针遍历数组
for (int i = 0; i < 5; i++) {
// 安全地通过指针访问数组元素
printf("%d ", *ptr);
ptr++; // 指针递增,移动到下一个元素
}
printf("\n");
// 恢复指针到数组的起始位置
ptr = arr;
// 尝试超出数组范围的操作
// 注意:以下代码是不安全的,仅用于示范如何避免
for (int i = 0; i < 5; i++) {
if (ptr < arr + 5) { // 确保指针没有超过数组末尾
printf("%d ", *ptr);
ptr++; // 安全递增指针
}
}
printf("\n");
return 0;
}
这个程序首先初始化一个指向数组第一个元素的指针。随后,通过一个 for 循环安全地遍历数组,每次迭代中,指针向前移动一个元素的位置。第一个循环简单地打印出数组的每个元素,而第二个循环在递增指针之前检查指针是否已经超出了数组的边界。
在上面的代码中,添加了一个检查 (ptr < arr + 5
) 来确保在解引用指针前,指针没有超出数组的边界。这种检查是防止指针越界的一种好方法,尤其在处理边界条件时非常有用。
5. 指针算术
5.1 指针加法
直接对指针进行加法操作通常是不允许的,如 * + *
是非法的。但可以将整数值加到指针上,这种操作称为指针的递增。当你增加一个整数到指针时,实际上是将指针向前移动该整数乘以指针所指类型大小的字节数。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
p = p + 3; // 将p向前移动3个int的位置,即p指向arr[3]
5.2 指针减法
与指针加法相对应,指针减法允许从指针中减去一个整数,即将指针向后移动指定的元素数量。同样,减去的整数会乘以指针指向类型的大小。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr + 4; // 指向arr的最后一个元素
p = p - 2; // 将p向后移动2个int的位置,即p指向arr[2]
5.3 指针差值
指针之间可以进行减法运算,用来计算两个指针之间的元素个数。这种运算的结果是两个指针之间的距离,以它们所指类型的单位计算。
int arr[5] = {10, 20, 30, 40, 50};
int *p1 = arr;
int *p2 = arr + 3;
int n = p2 - p1; // n 的值为 3,因为p2和p1之间有3个int类型的空间
5.4 指针比较
指针还可以进行比较运算,如检查两个指针是否相等(==)、不等(!=)、以及一个指针是否大于或小于另一个指针(<, >, <=, >=)。这些运算通常用于确保指针在有效范围内,或用于数组和其他数据结构的遍历。
int arr[5] = {10, 20, 30, 40, 50};
int *p1 = arr;
int *p2 = arr + 4;
if (p1 < p2) {
printf("p1 is before p2\n");
}
5.5 指针+-整数
#define N_VALUES 5
float values[N_VALUES]; // 定义一个包含5个浮点数的数组
float *vp; // 定义一个指向float的指针
// 指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0; // 将指针指向的当前元素设为0,并将指针向前移动到下一个元素
}
在这段代码中,vp 被初始化为指向数组 values 的第一个元素的地址(即 &values[0])。数组的名字(如 values)在不带下标的情况下也可以表示数组的第一个元素的地址,因此 &values[0] 可以简化为 values。
-
指针的递增 (*vp++ = 0): 这里使用了后缀递增运算符 ++,它的含义是先对指针进行解引用赋值操作,然后指针自增。指针的自增是基于它指向的数据类型的大小进行的。因为 vp 是一个指向 float 的指针,所以每次递增都会移动一个 float 的大小(通常是 4 字节)。这个操作确保了每个数组元素都被访问并设置为 0。
-
循环条件 (vp < &values[N_VALUES]): 这个条件检查 vp 是否还在数组 values 的有效范围内。&values[N_VALUES] 实际上指向数组最后一个元素之后的位置,这是一个常用技巧,用于确定指针是否已经处理完数组的所有元素。只要 vp 指向的地址小于这个边界,循环就继续执行。
5.6 指针-指针
int my_strlen(char *s)
{
char *p = s; // 初始化指针p为指向字符串s的开始位置
// 循环直到找到字符串的终止字符'\0'
while (*p != '\0')
p++; // 指针p逐字符向前移动
// 返回p和s之间的距离,即字符串的长度
return p - s;
}
在这个函数中,p 和 s 都是指向 char 的指针,它们指向同一个字符串的不同部分。
- 初始化:char *p = s; 这行代码将指针 p 初始化为指向由参数 s 指定的字符串的起始位置。
- 循环遍历字符串:while(*p != '\0') p++; 这是一个循环,它继续执行直到 *p(即 p 指向的当前字符)为 '\0',这是字符串的终止字符。在每次循环的末尾,p 递增,即向前移动到下一个字符。这种递增是按照指针指向的类型(这里是 char)进行的,对于 char 类型,每次递增移动一个字节。
- 计算长度:return p - s; 这行代码计算指针 p 和指针 s 之间的差值,这个差值表示的是两个指针之间的元素数量,即字符串的字符数。在 C 语言中,指针减法的结果给出两个指针之间的元素数量,这里是 p 和 s 之间的 char 数量,正好是字符串的长度。
5.7 指针和数组
#include <stdio.h>
int main()
{
// 定义一个整型数组 arr,并初始化
int arr[] = {1,2,3,4,5,6,7,8,9,0};
// 定义一个指针 p,并初始化指向 arr 的第一个元素
int *p = arr; // 指针存放数组首元素的地址
// 计算数组 arr 的元素数量
int sz = sizeof(arr) / sizeof(arr[0]); // sizeof(arr) 是整个数组的大小,sizeof(arr[0]) 是一个元素的大小
// 遍历数组,打印每个元素的地址
for (int i = 0; i < sz; i++)
{
// 打印数组元素的地址和通过指针加偏移量计算得到的地址
// &arr[i] 是取得数组第 i 个元素的地址
// p + i 是将指针 p 向前移动 i 个整型元素的距离
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p + i);
}
return 0;
}
这段代码中的 printf 调用展示了两种获取数组元素地址的方法&arr[i] 和 p + i是等效的:
- &arr[i] 直接使用数组索引获取第 i 个元素的地址。
- p + i 使用基指针 p,通过加上偏移量 i(这里的 i 是自动根据指针所指向的数据类型进行缩放的,因为 p 是指向 int 的指针,所以 p + i 相当于 p + i*sizeof(int))来获取相同的地址。