一、定义和声明指针
指针是一个特殊的变量,它存储另一个变量的内存地址。指针的声明需要指定它所指向的数据类型,这样编译器就可以正确地进行指针算术和类型转换。
1、定义指针(分配内存空间,并确定指针的类型)
int *ptr; // 定义一个指向整数的指针
char *cptr; // 定义一个指向字符的指针
这里,int *ptr 声明了一个指向 int 类型的指针,char *cptr 声明了一个指向 char 类型的指针。ptr 和 cptr 都是指针变量,它们存储了指向 int 和 char 类型数据的内存地址。
2、指针类型
指针类型决定了,指针在被解引用的时候所访问的权限,例如:
整型指针解引用会访问4个字节,字符指针解引用会访问1个字节
3、声明指针(告知编译器该指针的类型和存在,但不分配内存(当用 extern 关键字时))
4、定义与声明的结合
有时候,指针可能在一个地方定义,在其他地方声明。例如:
定义:
声明(在另一个文件中):
在这种情况下,ptr 在一个文件中定义,在另一个文件中声明,允许在不同的文件之间共享该指针。
二、指针的初始化
在使用指针之前,必须将其初始化为一个有效的地址。未初始化的指针会指向不确定的内存位置,可能导致程序崩溃或不确定的行为。
//-----------------------------------1.指针初始化为有效地址---------------------------------
int x = 10;
int *ptr = &x; // ptr 现在指向 x 的地址
//这里,&x 是取 x 的地址,ptr 现在存储了 x 的内存地址。通过 ptr,可以访问 x 的值。
//-----------------------------------2.指针初始化为NULL---------------------------------
int *ptr = NULL;
//在这种情况下,ptr 不指向任何有效的内存地址。使用 NULL 指针可以帮助避免对无效内存地址的操作。
//-----------------------------------3.动态分配内存---------------------------------
int *ptr = (int *)malloc(sizeof(int) * 10); // 分配足够存储 10 个 int 的内存
三、野指针(指针指向位置不可知)
1、野指针的成因
1.1 指针未初始化
int* p; // 未初始化指针
*p = 10; // p 是一个未初始化的指针,可能指向任意的内存地址。对其进行解引用会导致未定义行为。
1.2指针指向无效内存地址(局部变量、已释放的内存)
//-----------------------------------1.局部变量的地址---------------------------------
int* getPointerToLocal()
{
int localVar = 10; // 局部变量
return &localVar; // 返回局部变量的地址
}
int main()
{
int* p = getPointerToLocal(); // p 现在是野指针
printf("%d\n", *p); // 未定义行为
return 0;
}
//-----------------------------------2.已释放的内存---------------------------------
int* allocateMemory()
{
int* p = (int*)malloc(sizeof(int));
*p = 20;
return p;
}
int main()
{
int* p = allocateMemory();
free(p); // 释放内存
*p = 30; // 使用已释放的内存(野指针)
printf("%d\n", *p);
return 0;
}
1.3指针的越界访问
int main()
{
int array[3] = {1, 2, 3};
int* p = &array[3]; // 指向数组越界的位置, p指向了 array 的有效范围之外的位置,访问该位置会导致未定义行为
*p = 10; // 未定义行为
printf("%d\n", array[3]); // 未定义行为
return 0;
}
2、规避野指针的方法
①指针要初始化
②要注意指针越界的问题
③指针所指向的空间及时置NULL
④避免返回局部变量的地址
⑤指针使用之前检查有效性
四、解引用指针
解引用指针是访问指针所指向的内存地址中的值。使用解引用运算符 * 可以实现这一操作。
int x = 10;
int *ptr = &x;
printf("%d\n", *ptr); // 输出 10
*ptr 解引用 ptr,访问 ptr 指向的地址中的值。在这个例子中,*ptr 是 10,因为 ptr 指向 x。
五、指针运算
1. 加法和减法
ptr + n:将指针 ptr 向前移动 n 个元素的位置。
ptr - n:将指针 ptr 向后移动 n 个元素的位置。
int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr;
printf("Original value: %d\n", *ptr); // 10
printf("After adding 2: %d\n", *(ptr + 2)); // 30
printf("After subtracting 1: %d\n", *(ptr + 1)); // 20
2. 指针之间的减法
ptr1 - ptr2:计算两个指针 ptr1 和 ptr2 之间的元素差值,结果是它们之间相隔的元素个数。
int arr[] = {10, 20, 30, 40, 50};
int *ptr1 = &arr[1];
int *ptr2 = &arr[4];
printf("Difference: %d\n", ptr2 - ptr1); //3
3. 类型注意
指针运算是基于指针所指向的数据类型的大小进行的。例如,如果 ptr 是 int 类型的指针,ptr + 1 实际上会增加 sizeof(int) 个字节的位置。
六、常量指针和指针常量
1. 常量指针(const int* 或 int const*)
1.1 定义:
常量指针是指指针所指向的数据是常量的,不能通过这个指针修改所指向的值。
指针本身可以改变,意味着可以指向不同的内存地址。
1.2 示例:
void example()
{
int value1 = 10;
int value2 = 20;
const int* ptr = &value1; // 指向一个常量整型
// *ptr = 15; // 错误:无法通过 ptr 修改 value1
printf("Value1: %d\n", *ptr); // 输出: Value1: 10
ptr = &value2; // 合法:可以改变 ptr 指向的地址
printf("Value2: %d\n", *ptr); // 输出: Value2: 20
}
1.3 关键点:
const int* ptr 表示 ptr 是一个指向常量整数的指针,不能通过 ptr 修改 ptr 所指向的数据。
但 ptr 本身可以被重新赋值,即它可以指向其他变量。
2. 指针常量(int* const)
1.1 定义:
指针常量是指指针本身是常量的,不能改变指针的地址。
指针所指向的数据可以被修改。
1.2 示例:
void example()
{
int value1 = 10;
int value2 = 20;
int* const ptr = &value1; // 指针常量
*ptr = 15; // 合法:可以修改 value1 的值
printf("Value1: %d\n", *ptr); // 输出: Value1: 15
// ptr = &value2; // 错误:无法修改指针 ptr 指向的地址
}
1.3 关键点:
int* const ptr 表示 ptr 是一个指针常量,指针本身是常量,不能改变其指向的地址。
但通过 ptr 仍然可以修改 ptr 所指向的值。
3. 常量指针常量(const int* const)
1.1 定义:
常量指针常量是指指针本身和指针所指向的数据都是常量。
既不能修改指针的地址,也不能修改指针所指向的数据。
1.2示例:
void example()
{
int value = 10;
const int* const ptr = &value; // 常量指针常量
// *ptr = 15; // 错误:无法修改 value 的值
// ptr = &anotherValue; // 错误:无法修改 ptr 的地址
printf("Value: %d\n", *ptr); // 输出: Value: 10
}
1.3 关键点:
const int* const ptr 表示 ptr 既是常量指针(指针地址不可变),又是指向常量整数(数据值不可变)的指针。
4. 总结
常量指针(const int*):指针所指向的数据不可修改,但指针本身可以修改。
指针常量(int* const):指针本身不可修改,但指针所指向的数据可以修改。
常量指针常量(const int* const):指针本身和指针所指向的数据都不可修改。
七、指针数组和数组指针
1、指针数组 (Array of Pointers)
1.1 定义:指针数组是一个数组,数组的每个元素都是指向某种类型的指针。
1.2 内存布局:内存中,指针数组的每个元素都存储一个指针地址。
1.3 用途:常用于存储指向不同内存位置的指针,如指向字符串的指针数组。
int a = 10, b = 20, c = 30;
int* ptrAr r[3] = { &a, &b, &c }; // int* arr[3] : 表示 arr 是一个包含 3 个 int* 类型元素的数组
2、数组指针 (Pointer to an Array)
2.1 定义:数组指针是一个指针,它指向一个整个数组(而不是数组的单个元素)。
2.2 内存布局:指针本身存储的是整个数组的地址,操作时可以直接访问数组的所有元素。
2.3 用途:常用于需要通过指针操作整个数组的场景,比如传递固定大小的数组给函数。
int arr[3] = { 1, 2, 3 };
int (*arrPtr)[3] = &arr; // int (*ptr)[3] : 表示 ptr 是一个指向包含 3 个 int 元素的数组的指针
八、一级指针和二级指针
1、一级指针传参
void test(int* ptr) {
// 函数体
}
int main() {
int arr[10] = {0}; // 声明一个包含10个 int 元素的数组
int* p = arr; // arr 被隐式转换为 int* 类型,并赋值给 p
test(arr); // arr 被隐式转换为 int* 类型,传递给 test 函数
test(p); // p 已经是 int* 类型,直接传递
int a = 0;
test(&a); // &a 是 int* 类型,传递给 test 函数
return 0;
}
2、二级指针传参
void test(int* ptr) {
// 函数体
}
int main() {
int a = 0;
int* pa = &a;
int** ppa = &pa;
int* arr[5] = {0};
test(ppa);
test(&pa);
test(arr); // 传入arr相当于传入数组首元素的地址,而首元素类型是int*,那么int*的地址就是int**
return 0;
}