指针详解
目录:
- 理解指针
- const修饰指针
- 指针运算
- 野指针
- 传值调用和传址调用
- 使用指针访问数组
- 一维数组传参
- 二级指针
- 指针数组
- 字符指针变量
- 数组指针变量
- 二维数组传参
- 函数值指针变量
- 函数指针数组
- 转移表
- qsort函数模拟实现深度理解指针
1.理解指针
本质:指针其实就是地址,我们通常所说的指针其实是指针变量。
1.1 内存和地址
计算机的CPU(中央处理器)在处理数据的时候,需要的数据是在内存中提取的,计算后的数据也会放回内存中,内存也被划分为一个个的内存单元,每个内存单元是一个字节。
bit — 内存中最小的单位
Byte — 字节 1Byte = 8bit
KB
MB 1 MB = 1024KB
GB 1 GB = 1024MB
TB 1 TB = 1024GB
PB 1 PB = 1024TB
看到这里, 大家可能有个疑问,为什么内存的单元不是bit而是Byte呢?这是因为比特的单位过小,最小的char类型的大小也不过1个字节,如果用bite的话未免过度浪费内存的存储单元(内存是很贵的)。
内存的每个存储单元都有一个编号,也就是地址,CPU通过这个编号来准确的找到真个存储单元,在C语言中我们将内存存储单元的地址称为指针。
即:内存存储单元 == 地址 == 指针
1.2 内存的编址
cpu访问内存中的内容是通过访问地址完成的,如何规范并精确地访问,我们需要对地址进行编址,那到底是如何编编址地呢?这就要考虑到硬件知识了,CPU与内存的访问链接是通过线来完成的,其中一类线称为地址总线,32位机器有32位地址总线,每根线有两种状态,分别是有电压和无电压(用0,1表示),32根地址线一共有2^32种不同的状态,每一种状态代表一个地址,地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。
1.3 拆解指针
c语言中通过**&来获取变量的地址,通过*** 来解引用指针,获取地址中的值。
# include<stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
在 int* pa = &a;
pa左边写的是 int*
, 星号说明pa是指针变量,而前面的int是说明pa指向的是整型(int)类型的对象。
指针变量的大小
32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的⼤⼩就是8个字节。
# include<stdio.h>
int main()
{
printf("%zd\n", sizeof(char*));
printf("%zd\n", sizeof(short*));
printf("%zd\n", sizeof(int*));
printf("%zd\n", sizeof(double *));
return 0;
}
×86
×64
1.4 指针±整数
# include<stdio.h>
int main()
{
int n = 10;
char* pc = (char*)&n;
int* pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
int*
类型的+1跳过了4个字节,char*
类型+1跳过了1个字节,这便是指针变量类型的不同带来的差异,虽然指针变量都占用4字节的内存,但是不同类型决定了他们访问内存的能力,指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离).
1.5 void*指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的±整数和解引用的运算。
2.const修饰指针
const修饰指针变量,可以放在 ✳的左边,也可以放在✳的右边,但还是他们的意义是不一样的
# include<stdio.h>
// 测试const放在*的左边
void test1()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;
p = &m;
}
// 测试const放在*右边
void test2()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20;
p = &m;
}
// 测试两边都放
void test3()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20;
p = &m;
}
int main()
{
test1();
test2();
test3();
}
const修饰指针变量的时候:
- const 放在*的左边,修饰的是指针指向的内容,指针指向的内容不可变,指针本身可变
- const 放在*的右边,修饰的是指针变量本身,指针指向的内容可变,指针变量本身不可变
3.指针运算
3.1 指针±整数
# include<stdio,h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/ arr(arr[0]);
for(i = 0; i < sz; i++)
{
printf("%d ", *(p+i)); // *(p+i) = p[i]
}
return 0;
}
指针加减整数时,在内存地址上的作用效果是根据指针类型决定的,比如说int类型的指针+1后,内存中体跳过4个字节,这也体现了指针类型的意义。不同类型的指针访问地址的权限不同。
3.1 指针-指针
# include<stdio.h>
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0')
{
p++;
}
return p -s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
指针—指针的效果一般用于计算数组的长度
3.1 指针关系运算
# include<stdio,h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
while(p < arr+sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
4. 野指针
野指针指向的位置是不可知的(随机的,不正确的,没有明确的)
4.1野指针成因
4.1.1.指针未初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
# include<stdio.h>
int main()
{
int num = 0;
int *p1 = #
int *p2 = NULL;
return 0;
}
4.1.2 指针越界
指针访问到申请的空间以外的空间。
如何避免呢?
- 指针不在使用时,即使设置NULL,指针使用之前检查有效用
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的
时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,
同时使⽤指针之前可以判断指针是否为NULL。我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起来。不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使用。
# include<stio,h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = &arr[0];
int i = 0;
for(int i = 0; i < 10; i++)
{
*(p++) = i;
}
// 最后一次循环后指针越界
p = NULL; // 将指针设置为NULL
p = &arr[0];
if(p != NULL)
{
//
}
return 0;
}
- 避免返回局部变量的地址
5. 传值调用和传址调用
函数传参分为实参 和形参 ,形参仅仅是实参的一份临时拷贝,普通的传值调用无法通过形参的来改变实参。但传址调用时,传过去的是实参的地址,因此我们可以通过地址直接修改实参,
# include<stdio.h>
void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换");
swap(a,b);
printf("交换后是:%d %d", a,b);
return 0;
}
我们可以看到,a和b的位置并没有发生改变,因为我们用的是传值调用,形参只是实参的一份临时拷贝,我们无法通过形参来改变实参。
# include<stdio.h>
void swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换");
swap(&a,&b);
printf("交换后是:%d %d", a,b);
return 0;
}
这次改用传址调用后,位置发生了变化。
6.使用指针访问数组
6.1 数组名理解
数组名表示数组首元素的地址 ,但有两个特殊情况
- sizeof(数组名), 这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
- &数组名,数组名表示整个数组,取出整个数组的地址
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
发现结果是相同的,这是为什么呢?
&arr[0] 和arr相同我们不难理解,因为他们都是数组首元素的地址。&arr是取出的整个数组的地址,但是打印的时候仅仅显示出首元素的地址,当&arr + 1时跳过10个元素(40个字节)。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr+1);
return 0;
}
6.2 用指针访问数组
# include<stdio.h>
int main()
{
int arr[10] = {0}
int sz = sizeof(arr)/sizeof(arr[0]);
int *p = arr;
for(int i = 0; i < sz; i++)
{
scanf("%d", p+i);
}
// 输出
for(int i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
记: *(p+i) = p[i];
# include<stdio.h>
int main()
{
int arr[10] = {0}
int sz = sizeof(arr)/sizeof(arr[0]);
int *p = arr;
for(int i = 0; i < sz; i++)
{
scanf("%d", p+i);
}
// 输出
for(int i = 0; i < sz; i++)
{
printf("%d ", p[i];
}
return 0;
}
7.一维数组传参
一维数组传参,(将数组传递函数),在计算机设计的时候,为了节省空间,数组在作为参数传递给函数的时候,传递的本质是传递的指针,即数组首元素的地址,函数根据地址去寻找整个数组。
因此我们不可以在函数体的内部利用sizeof去计算数组的大小
#include <stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr)/sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int sz1 = sizeof(arr)/sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
8.二级指针
指针存放的时变量的地址,二级指针存放的是指针变量的地址,二级指针解引用获取指针变量的地址,再次解引用后得到变量。
# include<stdio.h>
int main()
{
int a = 0;
int *p = &a;
int **pa = &p;
return 0;
}
9.指针数组
指针数组是数组,以后判断的时候我们直接看结束语句。存放指针的数组。
指针数组模拟二维数组
# 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[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;
}
10.字符指针变量
# include<stdo.h>
int main()
{
char *pstr = "hello word!";
printf("%s\n", pstr)
return 0;
}
这里要要注意, char *pstr = "hello word!";
本质是将 "hello word!"
首字符的地址存放进了pstr里。
示例
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
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;
}
这⾥str3和str4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域,当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
11,数组指针变量
有之前所说,数组指针指的是指针,是指向数组地址的指针
int (*p)[10]; // 数组指针
int *p[10]; // 指针数组
int main()
{
int arr[20] = {0};
int (*p)[20] = &arr;
}
12.二维数组传参
我们知道,二维数组可以看作特殊的一维数组,二维数组的首元素是第一行的元素,在一般情况下:二维数组的数组名是第一行元素的地址。
# include<stdio.h>
void test(int (*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for(i = 0; i < r; i++)
{
for(j = 0; j < c; j++)
{
printf("%d ", *(*(p+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}});
test(arr,3,5);
return 0;
}
13.函数指针变量、
函数指针是指针,是存放函数地址的指针。
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
函数指针类型解析
使用:
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
int main()
{
int(*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(2, 3));
printf("%d\n", pf3(3, 5));
return 0;
}
14.函数指针数组
函数指针数组本质是一个数组,是数组的元素是函数指针
int (*p[5])();
int (*)( )是函数指针类型
函数指针数组是一个数组,其中的每个元素都是一个函数指针。函数指针用于指向具有特定签名的函数,允许在运行时动态选择和调用函数。函数指针数组特别适用于需要根据某些条件动态调用不同函数的场景,例如菜单系统、状态机或回调函数的实现。
-
函数指针:函数指针是一个指向函数的指针,允许通过指针调用该函数。
-
数组:函数指针数组是一个包含多个函数指针的数组,每个元素都是一个函数指针。
#include <stdio.h>
void function1() {
printf("Function 1\n");
}
void function2() {
printf("Function 2\n");
}
void function3() {
printf("Function 3\n");
}
int main() {
// 声明和初始化函数指针数组
void (*func_ptr_array[3])() = { function1, function2, function3 };
// 通过循环调用函数指针数组中的函数
for (int i = 0; i < 3; i++) {
func_ptr_array[i]();
}
return 0;
}
Function 1
Function 2
Function 3
15.转移表
转移表本质上是一个数组或集合,其中每个元素是一个函数指针。通过特定的索引值可以快速访问表中的某个函数并执行它。这种方法的主要优点是能够显著简化代码逻辑,并且在某些情况下可以提高执行效率。
函数指针数组实现:
#include <stdio.h>
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 div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输⼊有误\n");
}
} while (input);
return 0;
}
16.qsort函数模拟实现深度理解指针
qsort —- 使用冒泡排序的思想,模拟实现一下qsot
-
qsort怎么使用。
-
知道qsort怎么使用回调函数实现的通用的排序。
# include <stdio.h>
int Cmp(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
void Swap(char* buff1, char* buff2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char temp = *buff1;
*buff1 = *buff2;
*buff2 = temp;
buff1++;
buff2++;
}
}
void bobble_sort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2))
{
int i = 0;
for (i = 0; i < sz-1; i++)
{
int flag = 1; // 假设数组是排好序的
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}
void test4()
{
int arr[] = { 2,6,3,7,4,8,5,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bobble_sort(arr, sz, sizeof(arr[0]), Cmp);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
test4();
return 0;
}
width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char temp = *buff1;
*buff1 = *buff2;
*buff2 = temp;
buff1++;
buff2++;
}
}
void bobble_sort(void* base, int sz, int width, int(cmp)(const void e1, const void* e2))
{
int i = 0;
for (i = 0; i < sz-1; i++)
{
int flag = 1; // 假设数组是排好序的
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}
void test4()
{
int arr[] = { 2,6,3,7,4,8,5,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bobble_sort(arr, sz, sizeof(arr[0]), Cmp);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
test4();
return 0;
}