指针概念
●计算机中的内存被划分成一个个小的内存单元,每个内存单元大小是1个字节
●每个内存单元都是有地址的,这个地址就是指针,所以指针就是地址,地址就是指针
●地址就相当于门牌号,而内存单元就是房间,通过门牌号可以找到房间,这就是指针的含义
●指针和指针变量是不一样的,指针就是地址,是数据,而指针变量是变量,是用来存储地址数据的!
●口头上,喜欢将指针变量直接叫做指针,比如定义了一个指针变量,通常我们会说这是一个指针
指针大小
●32位机器,有32根地址线,每根地址线可以产生0/1两种信号,因此一共有2^32种组合,可以表示2^32种地址
●64位机器,有64根地址线,每根地址线可以产生0/1两种信号,因此一共有2^64种组合,可以表示2^64种地址
●指针变量是用来存放地址的,32位机器,地址占32个比特位(4字节),所以指针变量的大小就是4个字节; 64位机器,地址占64个比特位(8字节),所以指针变量的大小就是8个字节
●char占1个字节,int占4个字节,double占8个字节,而指针变量无论存放哪种类型变量的地址,固定占据4/8个字节,是不变的!
#include <stdio.h>
int main()
{
//打印结果都是4/8
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(double*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(long*));
printf("%d\n", sizeof(long long*));
return 0;
}
指针类型
指针类型含义:
●普通变量是有类型的, 比如int, double, float....., 而指针变量也是有类型的,比如int*, char*, double*,所以指针变量的定义是 type + * + 变量名
●int* 表明指针变量存储的是整形变量的地址, char* 表明指针变量存储的是字符型变量的地址, double* 表明指针变量存储的是双精度浮点型变量的地址
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
double b = 1.1;
double* pb = &b;
return 0;
}
指针类型的意义:
32位机器下,既然指针大小固定是4个字节,那无论什么类型的指针变量都能存下这个地址呀,那指针类型的意义何在?
●指针类型决定了指针解引用时访问权限有多大
整形指针解引用可以访问四个字节,字符指针解引用可以访问1个字节...
●指针类型决定了指针+-整数时可以跳过多少个字节
整形指针+1跳过4个字节,字符指针+1跳过1个字节,整形指针+2跳过2*4个字节,字符指针+2跳过2*1个字节
指针解引用
我们通过&变量名取出变量的地址,然后存储到指针变量里面,是为了后续方便通过指针变量找到指向的变量,就好比你记住了你同学的门牌号是为了后续方便串宿,那么如何通过指针变量找到指向的变量呢?? 就要用到 *(解引用) 操作符
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
*pa = 20;
printf("%d\n", a); //20
return 0;
}
注意: 定义指针变量时的*只是语法格式, 不是真正的运算符,而*变量名才是真正的解引用运算符!
const与指针变量
野指针
概念:野指针就是指向位置是不可知的(随机的、不正确的、没有明确限制的)指针。
野指针的成因
●指针未初始化
#include <stdio.h>
int main()
{
int* pa;
*pa = 10;
return 0;
}
如果指针没有初始化,那么指针指向的空间就是随机的,直接解引用访问就会有问题!
●指针越界访问
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i < 11; i++)
{
*p++ = i;
}
return 0;
}
p指向了不是用户申请的空间,就造成了越界访问!
●指针指向的空间被释放
#include<stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
return 0;
}
a是局部变量,定义在test函数的栈帧中,出了作用域就会销毁,而p还保存了销毁的空间中a的地址,就会导致p成为野指针
野指针的解决办法
●指针初始化
定义指针变量时如果明确知道存储哪个变量的地址,就写清楚&变量名,如果不确定指向的是哪块空间,就初始化成NULL
●小心指针越界
●指针指向的空间被释放后及时置为NULL
●使用指针之前检查有效性
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
if (pa != NULL)
{
*pa = 20;
printf("%d\n", a); //20
}
return 0;
}
指针运算
●指针 +- 整数
指针+-整数,表示指针向后/向前跳若干个字节,利用该运算,我们可以遍历数组
#include <stdio.h>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
int* p = arr; //数组名表示数组首元素地址
for (int i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
●指针 - 指针
指针-指针 计算的是两个指针之间的元素个数
#include <stdio.h>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
int* p1 = arr;
int* p2 = arr + 5;
printf("%d ", p2 - p1);
return 0;
}
用途: 模拟实现strlen
#include <stdio.h>
int mystrlen(const char* start)
{
const char* end = start;
while (*end) end++;
return end - start;
}
int main()
{
const char* str = "hello world";
printf("%d ", mystrlen(str)); //11
return 0;
}
●指针的关系运算
指针的关系运算指的是指针之间的大小比较, 比如我们要用指针将数组元素全部清0,有两种写法:
1. 从前向后清0, 指针与最后一个元素的下一个位置进行比较
#include <stdio.h>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
for (int* p = arr; p <= &arr[4]; p++)
{
*p = 0;
}
return 0;
}
2. 从后向前清0, 指针与第一个元素的前一个位置进行比较
#include <stdio.h>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
for (int* p = &arr[4]; p >= arr; p--)
{
*p = 0;
}
return 0;
}
推荐使用第一种方法,因为有标准规定:
允许数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
二级指针
●一级指针变量用来存储普通变量的地址
●一级指针变量也是变量,也有地址
●存储一级指针变量地址的指针变量就叫做二级指针变量
●一级指针变量解引用可以拿到指向的变量,二级指针变量解引用也可以拿到指向的变量 --- 一级指针变量,再解引用就可以拿到最开始定义的普通变量
#include <stdio.h>
int main()
{
int a = 10;
int* p1 = &a;
//是指针就要有*, 而p2指向的内容是p1, p1的类型是int*, 因此p2的类型是int**
int* *p2 = &p1;
**p2 = 20;
printf("%d ", a);
return 0;
}
字符指针
●字符指针是一个指针变量,存储了一个字符的地址
●字符指针可以保存一个字符串的起始地址(首字符)地址,借助字符指针可以打印字符串
#include <stdio.h>
int main()
{
const char* str = "hello world";
printf("%s", str);
return 0;
}
注意: str中保存的并不是"hello wolrd"整个字符串,"hello world" 是常量字符串,是位于字符串常量区的,而str是一个局部指针变量,是开辟在栈空间上的,只是str变量中存储了"hello world"中 'h' 的地址,因此可以直接打印出整个字符串
●一道面试题
#include <stdio.h>
int main()
{
char str1[] = "hello csdn.";
char str2[] = "hello csdn.";
char *str3 = "hello csdn.";
char *str4 = "hello csdn.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
结果:
str1 and str2 are not same
str3 and str4 are same
分析:
str1和str2是两个完全不同的数组,开辟在内存中的不同位置,而数组名表示数组首元素地址,自然地址不一样; 而常量字符串只有1份,因为常量字符串是只读的,不会被修改,只需要存储一份即可, 因此str3和str4指向的是同一个字符串,字符串的首地址当然一样
指针数组
●整形数组每个元素都是整形,字符数组每个元素都是字符...
int arr[5] = { 1,2, 3, 4, 5 };
char arr[3] = { 'a', 'b', 'c'};
●指针数组是一个数组,每个元素都是指针,保存的是地址
●整形指针数组的每个元素保存的都是整形变量的地址,字符指针数组的每个元素保存的都是字符变量的地址...
int a = 10, b = 20, c = 30;
int* arr[3] = { &a, &b, &c};
char c1 = 'a', c2 = 'b', c3 = 'c';
char* arr[3] = { &c1, &c2, &c3};
数组指针
●整形指针保存的是整形变量的地址,字符指针保存的是字符变量的地址...
●数组指针是一个指针变量,保存的是数组的地址
●数组地址 vs 数组首元素地址
1.&数组名取出的是整个数组的地址,sizeof(数组名)计算的是整个数组的大小,除此之外,数组名的含义都是数组首元素的地址
2.数组首元素地址 和 数组地址 在数值上是一样的,但是类型是不同的,导致了+-整数时跳过的字节数是不一样的,整形指针+1跳过的是4个字节,也就是一个整形变量,而数组指针+1跳过的是整个数组
●数组指针的类型写法
先明确三点:
1. 因为数组指针是一个指针,要有*,而[ ]的优先级高于*,我们用 () 将*p括起来, 保证是一个指针
2. 去掉变量名,便是一个变量的类型, 数组和函数也是同样如此
int a = 10; //去掉变量名a, a的类型是int
int* p = &a; //去掉变量名p, p的类型是int*
int arr1[5] = { 0 }; //去掉变量名arr1, arr1的类型是 int [5]
int* arr2[5] = { 0 }; //去掉变量名arr2, arr2的类型是 int* [5]
int add(int x, int y); //去掉函数名add, add的类型是 int (int, int)
3. 一个指针变量去掉变量名和*, 便是指针指向的内容的类型
int a = 10;
int* p1 = &a; //去掉p和*, p指向的变量类型是int
int** p2 = &p1; //去掉p2和*, p指向的类型是int*
接下来我们来分析一下数组指针的类型是如何写出来的:
#include <stdio.h>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
int(*p)[5] = &arr; //数组指针
return 0;
}
p是一个指针,因此要先和*结合,而实际是[ ]的优先级高于*, 因此给*p带上括号,而数组指针保存的是数组的地址,也就是指针指向的是一个数组,因此我们去掉*p之后,int [5]便是数组的类型
如果只去掉变量名p, 得到的就是变量的类型,可以看到, 数组指针变量的类型是 int (*) [5]
数组指针有一个用途,就是可以作为函数的参数接受实参二维数组, 打印二维数组
#include <stdio.h>
void Print(int (*arr)[5], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", *(*(arr + i) + j)); //后续有讲解
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7} };
Print(arr, 3, 5);
return 0;
}
数组传参
●一维数组传参
实参是一维数组(数组名), 形参形式上可以是数组名, 也可以是指针, 本质都是指针
#include <stdio.h>
void Print(int arr[]); //可以
void Print(int arr[5]); //可以
void Print(int* arr); //可以
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
Print(arr);
return 0;
}
●二维数组传参
数组名表示数组首元素的地址,对于二位数组来说,首元素就是第一行, 第一行是个一维数组,所以实参本质传递的是数组指针,形参可以用二维数组接收,也可以用数组指针接收
#include <stdio.h>
void Print(int arr[3][5]); //可以
void Print(int arr[3][]); //不可以, 行可以省略,列不能省略
void Print(int arr[][5]); //可以
void Print(int(*arr)[5]); //可以
int main()
{
int arr[3][5] = { {1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7} };
Print(arr);
return 0;
}
思考: 当函数形参是一级指针/二级指针时,实参我们可以传递什么呢?
●当函数形参是一级指针时,实参可以传递普通变量地址,一级指针变量,一维数组名, 只要实参本质是一级指针即可
#include <stdio.h>
void func(int* p);
int main()
{
int a = 10;
func(&a); //传变量地址
int* p = &a;
func(p); //传一级指针变量
int arr[10];
func(arr); //传一维数组名
return 0;
}
●当函数形参是二级指针时,实参可以传一级指针变量的地址,二级指针变量,一级指针数组的数组名,只要实参本质是二级指针即可
#include <stdio.h>
void func(int** p);
int main()
{
int a = 10;
int* p1 = &a;
func(&p1); //传一级指针变量的地址
int** p2 = &p1;
func(p2); //传二级指针变量
int* arr[10];
func(arr); //传一级指针数组的数组名
return 0;
}
函数指针
类比之前的整形指针、字符指针等等,可以知道函数指针是一个指针变量,保存的是函数的地址
●函数指针类型
依旧明确三点:
1. 函数指针是一个指针,要有*,而 () 的优先级高于*,我们用 () 将 *pf 括起来, 保证是一个指针
2. 去掉变量名,便是一个变量的类型, 数组和函数也是同样如此
3. 一个指针变量去掉变量名和*, 便是指针指向的内容的类型
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = &add; //函数指针
return 0;
}
add函数的类型是 int (int, int), pf是一个指针变量,所以(*pf), 去掉*和pf, 剩余的应该是pf指向的内容的类型,也就是int (int, int)
●函数指针使用
1. &函数名 和 函数名 都是函数的地址
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf1)(int, int) = &add; //函数指针
int (*pf2)(int, int) = add; //函数指针
return 0;
}
2. 根据指针变量的用法,pf1保存的是&add, 所以可以通过 解引用拿到add, 调用add函数
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf1)(int, int) = &add; //函数指针
int ret1 = (*pf1)(3, 5); //()的优先级大于*, 因此要给*pf1加()
printf("%d ", ret1); //8
return 0;
}
3.由于函数名本身就是函数的地址,直接将add赋值给指针变量,也就意味着不用解引用,可以直接通过指针变量调用函数!
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf2)(int, int) = add; //函数指针
int ret1 = pf2(3, 5);
printf("%d ", ret1); //8
return 0;
}
4.既然指针变量不用解引用可以直接访问到add函数,那么函数指针解引用中的*就没有任何意义了,只是个形式而已, 因此无论写多少个*, 都能得到正确结果!
#include <stdio.h>
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf1)(int, int) = &add; //函数指针
int ret1 = (*********pf1)(3, 5); //*其实没有意义
printf("%d ", ret1); //8
return 0;
}
函数指针数组
函数指针数组是一个数组,数组的每个元素类型是函数指针类型
int (*pf[5])(int, int); //函数指针数组,数组有5个元素,每个元素类型是int (int, int)
[ ]的优先级高于*, 因此pf先和[ ]结合,表明是一个数组,去掉数组名和元素个数,剩下的int (int, int) 就是数组每个元素的类型,是函数指针类型; 去掉数组名,就是数组的类型,所以函数指针数组的类型是int (* [5]) (int, int)
函数指针数组有什么用呢??? 比如我们下面实现一个简单的计算器
#pragma warning(disable:4996)
#include <stdio.h>
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;
}
void menu()
{
printf("##########################################\n");
printf("########### 1. add 2.sub ##############\n");
printf("########### 3. mul 4.div ##############\n");
printf("########### 0. exit ##############\n");
printf("##########################################\n");
}
int main()
{
int input = 0;
int (*pfArr[5])(int, int) = { 0, add, sub, mul, div}; //函数指针数组
do
{
menu();
int x = 0, y = 0, ret = 0;
printf("请选择: \n");
scanf("%d", &input);
if (input == 0)
{
printf("退出程序\n");
break;
}
else if (input >= 1 && input <= 4)
{
printf("请输入两个操作数: \n");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("结果是: %d: \n", ret);
}
else
{
printf("选择错误, 请重新选择\n");
}
} while (input);
return 0;
}
如果不使用函数指针数组,使用switch case语句也是可以的,但是case语句中会有多份重复代码,比如提示用户输入两个操作数,打印计算结果等等,代码冗余,用函数指针数组就很好的解决了该问题!
指向函数指针数组的指针
有函数指针数组,就必然存在指向函数指针数组的指针,类别之前的推导方法,"指向函数指针数组的指针"是一个指针,指针中存放了一个数组的地址,该数组是函数指针数组而已!
int (*(*pf)[5]) (int, int); //指向函数指针数组的指针
*pf表明是指针变量,去掉*和pf之后,就是指针变量指向的内容的类型,也就是int (* [5]) (int, int), 是函数指针数组类型
回调函数
百度: 回调函数就是一个被作为参数传递的函数
简单来说,现在有A函数和B函数,将A函数的地址传递给B函数的参数(函数指针), 在B函数中通过函数指针调用A函数,这就是回调机制!
回调函数应用:
比如刚才实现的一个简单的计算器,如果不使用函数指针数组,我们也可以用switch-case + 回调函数来实现
#pragma warning(disable:4996)
#include <stdio.h>
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;
}
void menu()
{
printf("##########################################\n");
printf("########### 1. add 2.sub ##############\n");
printf("########### 3. mul 4.div ##############\n");
printf("########### 0. exit ##############\n");
printf("##########################################\n");
}
int calc(int (*pf)(int, int))
{
int x = 0, y = 0;
printf("请输入两个操作数: \n");
scanf("%d %d", &x, &y);
int ret = pf(x, y);
return ret;
}
int main()
{
int input = 0;
int (*pfArr[5])(int, int) = { 0, add, sub, mul, div }; //函数指针数组
do
{
menu();
int x = 0, y = 0, ret = 0;
printf("请选择: \n");
scanf("%d", &input);
switch(input)
{
case 0:
printf("退出程序\n");
break;
case 1:
ret = calc(add);
printf("结果是: %d\n", ret);
break;
case 2:
ret = calc(sub);
printf("结果是: %d\n", ret);
break;
case 3:
ret = calc(mul);
printf("结果是: %d\n", ret);
break;
case 4:
ret = calc(div);
printf("结果是: %d\n", ret);
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
自定义了一个calc函数,就是专门用来计算的,具体做的是加减乘除哪一个,根据选择input, 将add/sub/mul/div的地址传入,在calc中进行回调即可, 返回计算结果
回调函数非常典型的应用就是C标准库中qsort(快排)的最后一个参数
void qsort (void* base, size_t num, size_t size,
int (*compar)(const void*,const void*));
参数说明:
●第一个参数是待排序的内容的起始位置
●第二个参数是从起始位置开始,待排序的元素个数
●第三个参数是待排序的每个元素的大小,单位是字节;
●第四个参数是一个函数指针, 该函数指针指向的函数的两个参数的参数类型均为const void*,返回类型为int。当参数e1小于参数e2时返回小于0的数;当参数e1大于参数e2时返回大于0的数;当参数e1等于参数e2时返回0
我们知道,排序的对象可能会有各种类型,比如整形,浮点型,其他自定义类型(结构体等),而实现qsort库函数的作者并不知道你将来要使用qsort排序什么样类型的数据,因此将qsort第四个参数设置成了函数指针,由用户自己传入比较规则的函数, 实现排序, 并且qsort默认是升序,如果要排降序,将自定义函数的两个参数的前后顺序换一下即可!
说明: void*指针好比垃圾桶,可以接收任意类型的指针,但不能直接解引用或者++, 因为void的大小是未定义的,使用时需要自己强制类型转化!
qsort使用:
排序整形数据:
#include <stdio.h>
#include <stdlib.h>
int cmp(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void PrintArray(int* arr)
{
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]); //1 2 3 4 5
}
}
int main()
{
int arr[5] = { 4, 2, 3, 1, 5 };
qsort(arr, 5, sizeof(int), cmp);
PrintArray(arr);
return 0;
}
排序结构体
●按年龄字段排序
#include <stdio.h>
#include <stdlib.h>
struct stu
{
char name[20];
int age;
};
int cmp(const void* e1, const void* e2)
{
return (*(struct stu*)e1).age - (*(struct stu*)e2).age;
}
int main()
{
struct stu s[3] = { {"zhangsan", 20}, {"lisi", 22}, {"wangmazi", 18}};
qsort(s, 3, sizeof(struct stu), cmp);
for (int i = 0; i < 3; i++)
{
printf("%d ", s[i].age); //18 20 22
}
return 0;
}
●按名字字段排序
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct stu
{
char name[20];
int age;
};
int cmp(const void* e1, const void* e2)
{
return strcmp( ((struct stu*)e1)->name, ((struct stu*)e2)->name );
}
int main()
{
struct stu s[3] = { {"zhangsan", 20}, {"lisi", 22}, {"wangmazi", 18}};
qsort(s, 3, sizeof(struct stu), cmp);
for (int i = 0; i < 3; i++)
{
printf("%s ", s[i].name); //lisi wangmazi zhangsan
}
return 0;
}
模拟qsort实现一个通用类型的冒泡排序
之前冒泡排序的参数类型我们是写死的,基本都是int, 而现在了解了库中qsort的用法,我们也可以仿照库中qsort的参数设计,写一个通用的冒泡排序算法,可以排序任意类型的数据
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct stu
{
char name[20];
int age;
};
int cmp(const void* e1, const void* e2)
{
return strcmp( ((struct stu*)e1)->name, ((struct stu*)e2)->name );
}
void swap(char* buf1, char* buf2, int width)
{
//一个字节一个字节地交换
for (int i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void* base, int sz, int width,
int (*compare)(const void*, const void*))
{
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - i - 1; j++)
{
if (compare((char*)(base)+j * width, (char*)(base)+(j + 1) * width) > 0)
{
swap( (char*)(base)+j * width, (char*)(base)+(j + 1) * width, width);
}
}
}
}
int main()
{
struct stu s[3] = { {"zhangsan", 20}, {"lisi", 22}, {"wangmazi", 18}};
bubble_sort(s, 3, sizeof(struct stu), cmp);
for (int i = 0; i < 3; i++)
{
printf("%s ", s[i].name); //lisi wangmazi zhangsan
}
return 0;
}
说明:
●冒泡排序函数内 if 语句判断两个元素大小时,需要能够确定比较的是哪两个元素,而bubble_sort函数不知道传进来的是什么类型,因此我们先将 base强制类型转化成char*, 再加上j*width(每个元素的大小), 就能得到第j个元素的起始地址,第j+1个元素同样如此
●swap函数中,我们也不知道交换的数据是什么类型,因此我们可以一个字节一个字节的进行交换,一共交换多少个字节,就要看一个数据多大了,因此我们将width参数也传递进了swap函数