一、指针基础知识
1. 内存和地址
- 在计算机中,内存被组织成字节的线性序列,每个字节都有一个唯一的地址。地址通常以十六进制形式表示。
- 我们可以简单理解为:内存单元的编号 == 地址 == 指针
2. 指针变量和地址
指针变量用来存储内存地址。例如:
int var = 10;
int *p = &var; // p 存储了 var 的地址,&为取地址符
3. 指针变量类型的意义
指针的类型决定了它所指向的变量类型,以及解引用时的解释方式。例如:
int *ip; // 指向 int
double *dp; // 指向 double
4. const修饰指针
const
可以修饰指针,使其指向的值不可变,const在解引用即*之前还是之后要做出区分:
const int *p; // const在*左边
int const *p; // 同上,const在*左边
int *const p; // const在*右边
- const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。
- const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
5. 指针运算
指针的基本运算有三种,分别是:
- 指针+- 整数
- 指针-指针
- 指针的关系运算
6. 野指针
野指针是指向未定义或已释放内存的指针,使用它们可能导致程序崩溃:
int *p; // 未初始化的指针,可能指向任何地方
我们应尽量规避野指针的出现,主要有以下几种方法:
①指针初始化
- 如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL。
- NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
- 初始化如下:
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;
return 0;
}
② 小心指针越界
- 一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
③ 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
- 当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的一个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL。
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
for(i=0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使用的时候,判断p不为NULL的时候再使用
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}
④ 避免返回局部变量的地址
- 不要返回局部变量的地址。
7. assert断言
assert
用于检查程序中的条件,如果条件为假,则程序终止,使用需要包涵头文件<assert.h>,可用于验证变量p 是否等于NULL。
#include <assert.h>
int main()
{
int *p == NULL;
assert(p != NULL); // 如果指针p不为 NULL,程序将终止
}
8. 指针的使用和传址调用
通过指针,可以在函数中修改外部变量的值:
void increment(int *p)
{
(*p)++;
}
int main()
{
int x = 0;
increment(&x);
printf("%d\n", x); // 输出 1
}
二、指针与一维数组的联系
1. 数组名的理解
数组名在大多数表达式中被视为指向数组首元素的指针:
int arr[5];
int *p = arr; // p 指向 arr 的第一个元素
2. 使用指针访问数组
指针可以用于遍历数组:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for (int i = 0; i < 5; i++)
{
printf("%d\n", *(p + i)); // 输出数组元素
}
3. 一维数组传参的本质
数组作为参数传递给函数时,实际上是传递了数组首元素的地址:
void printArray(int *parr, int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", *(parr + i));
}
printf("\n");
}
int main()
{
int myArray[] = {1, 2, 3};
printArray(myArray, 3);
}
4. 字符指针变量
字符指针常用于字符串操作:
char *str = "Hello, World!";
printf("%s\n", str);
三、指针与二维数组的联系
1. 二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放的地方就是二级指针
int a = 0;
int * pa = &a
int ** pa = &pa
**ppa 先通过*ppa 找到pa ,然后对pa 进行解引用操作: *pa ,那找到的是a .
**ppa = 0;
//等价于*pa = 0;
//等价于a = 0;
2. 数组指针变量
数组指针变量指向一个数组:
int arr[5] = { 1,2,3,4,5 };
int (*p)[5] = &arr
3. 指针数组
指针数组的每个元素是地址,又可以指向一块区域。
4. 指针数组模拟二维数组
#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*的,就可以存放在parr数组中
int* parr[3] = {arr1, arr2, arr3};
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。
5. 二维数组传参的本质
二维数组作为函数参数时,实际上是传递了指向数组首行的指针。
四、指针与函数的联系
1. 函数指针变量
如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实和数组指针非常类似。如下:
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
2. 函数指针数组
把函数的地址存到一个数组中,那这个数组就叫函数指针数组:
int (*parr[3])();
五、指针的应用
1. 冒泡排序
冒泡排序通过交换相邻元素实现排序,可以使用指针简化代码:
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
int i = 0;
for(i=0; i<sz-1; i++)
{
int j = 0;
for(j=0; j<sz-i-1; j++)
{
if(arr[j] > arr[j+1])
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
2. 回调函数
回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数
时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
void doSomething(int n, void (*callback)(int))
{
// 处理 n
callback(n);
}
void callbackFunction(int n)
{
printf("Callback with %d\n", n);
}
int main() {
doSomething(10, callbackFunction);
}
3. qsort使用举例
qsort
是C标准库中的快速排序函数,使用函数指针指定比较规则:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//整型比较
int cmp_int(void *p1, void *p2)
{
return *(int*)p1 - *(int*)p2;
}
//字符比较
int cmp_char(void* p1, void* p2)
{
return *(char*)p1 - *(char*)p2;
}
//整型打印
int Print_int(int* parr, int len)
{
int i = 0;
for (; i < len; i++)
{
printf("%d ", *(parr + i));
}
printf("\n");
}
//字符打印
int Print_char(char* pstr, int len)
{
int i = 0;
for (; i < len; i++)
{
printf("%c ", *(pstr + i));
}
printf("\n");
}
int main()
{
int arr[] = { 5,6,8,9,7,3,2,1,0,4 };
char str[] = "zkeorwpqs";
int len1 = sizeof(arr) / sizeof(arr[0]);
int len2 = strlen(str);
qsort(arr, len1, sizeof(arr[0]), cmp_int);
qsort(str, len2, sizeof(str[0]), cmp_char);
Print_int(arr, len1);
Print_char(str, len2);
return 0;
}
4. qsort函数的模拟实现
使用回调函数,模拟实现qsort(采用冒泡的方式):
//模仿qsort的功能实现一个通用的冒泡排序
#include<stdio.h>
#include<string.h>
//结构体创建
struct Stu
{
char str[1000];
int score;
};
//数据交换
void Swap(char* p1, char* p2, int len)
{
int i = 0;
for (; i < len; i++)
{
char tmp = *(p1 + i);
*(p1 + i) = *(p2 + i);
*(p2 + i) = tmp;
}
}
//通用冒泡排序函数
void Bubble_sort(char* base, int mun, int len, int (*com)(void* p1, void* p2))
{
int i = 0,j = 0;//趟数
for (; i < mun - 1; i++)
{
for (j = 0; j < mun - 1; j++)
{
int compar = com(base + j * len, base + (j + 1) * len);
if (compar > 0)
{
Swap(base + j * len, base + (j + 1) * len, len);
}
}
}
}
//整型比较
int cmp_int(void *p1, void *p2)
{
return *(int*)p1 - *(int*)p2;
}
//字符比较
int cmp_char(void* p1, void* p2)
{
return *(char*)p1 - *(char*)p2;
}
//结构体整型比较
int cmp_Str_int(void* p1, void* p2)
{
return ((*(struct Stu*)p1).score) - ((*(struct Stu*)p2).score);
}
//结构体字符串比较
int cmp_Str_str(void* p1, void* p2)
{
return strcmp(((struct Stu*)p1)->str,((struct Stu*)p2)->str);
}
//整型打印
void Print_int(int* parr, int len)
{
int i = 0;
for (; i < len; i++)
{
printf("%d ", *(parr + i));
}
printf("\n");
}
//字符打印
void Print_char(char* pstr, int len)
{
int i = 0;
for (; i < len; i++)
{
printf("%c ", *(pstr + i));
}
printf("\n");
}
//结构体打印
void Print_struct(struct Stu* pa, int len)
{
int i = 0;
for (; i < len; i++)
{
printf("%s ", (pa + i)->str);
}
printf("\n");
}
int main()
{
//整型数组排序
int arr[] = { 5,6,8,9,7,3,2,1,0,4 };
int len1 = sizeof(arr) / sizeof(arr[0]);
Bubble_sort(arr, len1, sizeof(arr[0]), cmp_int);
Print_int(arr, len1);
//字符数组排序
char str[] = "zkeorwpqs";
int len2 = strlen(str);
Bubble_sort(str, len2, sizeof(str[0]), cmp_char);
Print_char(str, len2);
//结构体排序
struct Stu s[] = { {"abcde",96},{"bcdef",97},{"cdefg",89},{"defgh",92},{"efghi",94} };
int len3 = sizeof(s) / sizeof(s[0]);
//按其中整型排序
Bubble_sort(s, len3, sizeof(s[0]), cmp_Str_int);
Print_struct(s, len3);
//按其中字符串排序
Bubble_sort(s, len3, sizeof(s[0]), cmp_Str_str);
Print_struct(s, len3);
return 0;
}
5. 转移表
函数指针数组的用途:转移表
举例:计算器的一般实现:
#include<stdio.h>
void Menu()
{
printf("******** 计算器 *******\n");
printf("******** 1.相加 *******\n");
printf("******** 2.相减 *******\n");
printf("******** 3.相乘 *******\n");
printf("******** 4.相除 *******\n");
printf("******** 0.退出 *******\n");
printf("***************************\n");
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int input = 0;
int x = 0, y = 0;
int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };
do
{
Menu();
printf("请选择:");
scanf("%d", &input);
if (input > 0 && input < 5)
{
printf("请输入两个操作数:");
scanf("%d%d", &x, &y);
int ret = pfArr[input](x, y);
printf("结果为:%d\n", ret);
}
else if (input == 0)
{
printf("已退出计算器!");
}
else
{
printf("无效输入,请重新输入!\n");
}
}
while(input);
return 0;
}
六、总结
指针是C语言的核心特性之一,它们提供了对内存的直接操作能力。正确使用指针可以提高程序的性能和灵活性,但同时也需要小心处理,以避免野指针等常见错误。