C语言学习——从零到进阶深入学习指针

指针的基本概念

在聊起指针的基本概念之前,我们先清楚内存和地址的概念

        内存和地址

        计算机上的CPU在处理数据的时候,需要的数据都是在内存中读取的,处理后的数据也会放回内存中,这些内存也当然需要高效的管理,那么,它们是如何实现的呢?

        其实就是把内存划分为一个一个的内存单元,每个内存单元的大小取一个字节,每个内存都有自己的编号,也就是地址,便于计算机高效的进行访问,不会像无头的苍蝇一样。

       指针的基本概念

        在C语言中,指针是一种特殊的变量,其值是内存地址。指针使得程序能够直接访问和操作内存中的数据。        

        在了解了内存和地址的基本概念后,指针其实就是地址,就是C语言给地址起了个新的名字:叫指针,所以可以理解为:内存单元的编号 == 地址 == 指针。

指针的使用

指针的使用是通过取地址符号(&)来实现的

#include <stdio.h>
int main()
{
 int a = 10;
 &a;//取出a的地址
 printf("%p\n", &a);
 return 0;
}

获得了变量的地址后,怎么使用呢?这里需要引入一个新的操作符:解引用操作符(*)

#include <stdio.h>
int main()
{
 int a = 100;
 int* pa = &a;
 *pa = 0;
 return 0;
}
指针变量的大小

 指针变量的大小取决于地址的大小,在不同位的平台下,地址的大小也是不同的。在32位平台下地址是32个bit位,即4个字节,在64位平台下地址是64个bit位,即8个字节。

我们可以使用sizeof操作符在计算指针变量的大小

#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
 printf("%zd\n", sizeof(char *));
 printf("%zd\n", sizeof(short *));
 printf("%zd\n", sizeof(int *));
 printf("%zd\n", sizeof(double *));
 return 0;
}
指针 +- 整数
#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;
}

我们可以看出,char* 类型的指针变量 +1跳过1个字节,int* 类型的指针变量+1跳过了4个字节,这就是指针变量类型的差异带来的变化。

结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

野指针成因
        指针未初始化
#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}
        指针越界访问
#include <stdio.h>
int main()
{
 int arr[10] = {0};
 int *p = &arr[0];
 int i = 0;
 for(i=0; i<=11; i++)
 {
 //当指针指向的范围超出数组arr的范围时,p就是野指针
 *(p++) = i;
 }
 return 0;
}
        指针指向的空间释放
#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
}
如何规避野指针
        指针初始化

如果明确知道指针指向哪里就直接赋值地址,如果不知道应该指向哪里,可以给指针赋值NULL

#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;
 
 return 0;
}
        小心指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。

        指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。

int main()
{
 int arr[10] = {1,2,3,4,5,67,7,8,9,10};
 int *p = &arr[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;
}
        避免返回局部变量的地址

如造成野指针的第3个例⼦,不要返回局部变量的地址。

通过指针访问数组

我们都知道数组名就是数组首元素的地址,所以可以把它理解为一个指针

#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);
 return 0;
}

这里我们看到,数组名的地址和数组首元素的地址是一样的。

有了前⾯知识的⽀持,再结合数组的特点,我们就可以很⽅便的使⽤指针访问数组了

#include <stdio.h>
int main()
{
 int arr[10] = {0};
 //输⼊
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //输⼊
 int* p = arr;
 for(i=0; i<sz; i++)
 {
 scanf("%d", p+i);
 //scanf("%d", arr+i);//也可以这样写
 }
 //输出
 for(i=0; i<sz; i++)
 {
 printf("%d ", *(p+i));
 }
 return 0;
}

这个代码搞明⽩后,我们再试⼀下,如果我们再分析⼀下,数组名arr是数组⾸元素的地址,可以赋值 给p,其实数组名arr和p在这⾥是等价的。那我们可以使⽤arr[i]可以访问数组的元素,那p[i]是否也可 以访问数组呢?

#include <stdio.h>
int main()
{
 int arr[10] = {0};
 //输⼊
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //输⼊
 int* p = arr;
 for(i=0; i<sz; i++)
 {
 scanf("%d", p+i);
 //scanf("%d", arr+i);//也可以这样写
 }
 //输出
 for(i=0; i<sz; i++)
 {
 printf("%d ", p[i]);
 }
 return 0;
}

通过以上的例子,一维数组传参的本质我们也就搞清楚了,本质上传的就是地址,也就是通过指针传递的。

总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

指针数组

指针数组是指针还是数组?

我们类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组。

那指针数组呢?是存放指针的数组。

其实指针数组类似于二维数组,这里我们可以模拟一下

#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;

各种指针变量

        字符指针变量

在指针的类型中我们知道有⼀种指针类型为字符指针 char* 

int main()
{
 char ch = 'w';
 char *pc = &ch;
 *pc = 'w';
 return 0;
}
int main()
{
 const char* pstr = "hello.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
 printf("%s\n", pstr);
 return 0;
}

 代码 const char* pstr = "hello."; 特别容易让同学以为是把字符串 hello. 放 到字符指针 pstr ⾥了,但是本质是把字符串 hello. ⾸字符的地址放到了pstr中。

        数组指针变量

之前我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。

数组指针变量是指针变量?还是数组?

答案是:指针变量。

 int (*p)[10];

解释:p和*结合,说明p是一个指针变量,然后指向一个大小为10的整型数组。所以p是一个指针,指向一个数组,叫数组指针。

        函数指针变量

函数指针变量就是用来存放函数地址的,未来通过地址能够调用函数的

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写上或者省略都是可以的
        函数指针变量的使用
#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;
}
        两段有趣的代码
 (*(void (*)())0)();
void (*signal(int , void(*)(int)))(int);
函数指针数组

那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];

转移表

#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;
}

qsort函数

quick sort即为快速排序。

#include <stdio.h>
//qosrt函数的使⽤者得实现⼀个⽐较函数
int int_cmp(const void * p1, const void * p2)
{
 return (*( int *)p1 - *(int *) p2);
}
int main()
{
 int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
 int i = 0;
 
 qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
 for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
 {
 printf( "%d ", arr[i]);
 }
 printf("\n");
 return 0;
}

使用qsort排序结构体类型

struct Stu //学⽣
{
 char name[20];//名字
 int age;//年龄
};
//假设按照年龄来⽐较
int cmp_stu_by_age(const void* e1, const void* e2)
{
 return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//strcmp - 是库函数,是专⻔⽤来⽐较两个字符串的⼤⼩的
//假设按照名字来⽐较
int cmp_stu_by_name(const void* e1, const void* e2)
{
 return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
//按照年龄来排序
void test2()
{
 struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
 int sz = sizeof(s) / sizeof(s[0]);
 qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
//按照名字来排序
void test3()
{
 struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
 int sz = sizeof(s) / sizeof(s[0]);
 qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
 test2();
 test3();
 return 0;
}

总结C语言指针

指针是C语言中用于存储变量地址的特殊变量。通过指针,可以直接访问和修改内存中的数据。指针的高级应用包括动态内存管理、操作数组、函数参数传递、以及实现函数和数据的灵活操作。正确使用指针能提高程序的效率和灵活性,但也需要注意避免野指针和内存泄漏等问题

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值