C语言 指针十八罗汉 ! ! !

前言

本文主要讲解指针的多种用法,和一些概念,不过多赘述一些函数的用法,全文5400多字,根据需要查漏补缺, 如有疑问可以随时私信@张三xy,会第一次时间进行更新补充和说明

形而上学者谓之道,形而下学者谓之器


目录

前言

一、指针的基本概念

二、指针类型和指针运算

三、多级指针

四、万能指针

五、野指针

六、悬垂指针

七、空指针

八、指针和数组

九、指针数组和数组指针

十、指针和字符数组

十一、指针和动态内存 堆和栈

十二、动态内存函数 malloc calloc relloc 和free的使用

十三、内存泄露

十四、函数返回指针

十五、函数指针

十六、回调函数


一、指针的基本概念

指针的特点

        (1) 表示一些复杂的数据结构
        (2) 快速的传递数据
        (3) 使函数返回一个以上的值
        (4) 能直接访问硬件
        (5) 能够方便处理字符串
        (6) 是理解面向对象语言中引用的基础

        指针是C语言的灵魂

指针的定义
        地址
                内存单元的编号
                从零开始的非负整数
                范围32位支持最多4G(64位计算机支持128G,32个4G)

        注意(!!!):
                一个指针变量,无论它指向的变量占几个字节,
                 在32位的计算机上,占4个字节;
                在64位的计算机上,占8个字节。
                 一个指针占几个字节,等于是一个地址的内存单元编号有多长


        指针变量

                指针就是地址,地址就是指针
                地址就是内存单元的编号
                指针变量是存放地址的变量
                指针和指针变量是两个不同的概念
                注意:通常叙述时会把 指针变量 简称为 指针,实际上两者含义不同
                指针的本质就是一个操作受限的非负整数

指针的分类


        


二、指针类型和指针运算

指针变量的运算

        (1) 两个指针变量之间 不能相加    不能相乘    也不能相除(同一类型的指针可以相互赋值)
         (2) 若两个指针变量指向的是同一块连续空间,且是同类型,则这两个指针变量才可以相减

int a[5] = {1,2,3,4,5};
int *p = &a[0];
int *q = &a[4];
printf("p 和 q所指向的单元相隔 %d 个 单元\n",q-p);

结果: p 和 q所指向的单元相隔 4 个 单元

         (3) 指针 + n (表示往后移动  (数据类型字节大小) * n)

int i = 0;
int *p = &i;
printf("%d\n",p);
printf("%d\n",p+1);

结果: p 和 p + 1的差值一定是 4

代码案例

int a[2] = {3,9};
int * p = &a[0];
printf("*p = %d",*(p+1));

结果: *p = 9


三、多级指针

多级指针的概念

        一个指针变量指向的是另一个指针变量,我们就称它为二级指针,如此推理可以无限套娃

int i = 3;
//p 是指向变量 i 地址的指针
int * p = &i;
//q 是指向 指针p 地址的指针
int ** q = &p;
**q =666;
printf("i = %d",i);

结果:通过操作二级指针q得到, i = 666

四、万能指针

万能指针(void 类型指针)

        万能类型指针可以接收任意类型变量的内存地址 在通过万能指针修改变量时, 需要把万能指针转换为变量对应的指针的类型

int a = 10;
//1.定义万能类型指针 指向a变量地址
void * p = &a;
//2.把万能类型指针 强制转换成对应的数据类型    
*(int*)p = 666;
printf("a = %d",a);

结果:通过操作万能指针p得到, a = 666


五、野指针

野指针

        某些编程语言允许未初始化的指针的存在,而这类指针即为野指针
        例如:int * p = 100;
        指针变量指向了一个未知的空间,操作系统将0-255作为系统占用不允许访问操作, 操作野指针对应的空间可能报错

int * p;
*p = 6;//这就是野指针,指针未初始化指向有效空间,就使用了

六、悬垂指针

悬垂指针的概念       

        该指针指向曾经存在的对象,但该对象已经不再存在了,此类指针称为悬垂指针
     

常见的悬垂指针错误

        栈分配的局部变量的地址时,一旦调用的函数返回,分配给这些变量的空间将被回收,此时它们拥有的是"垃圾值"

#include <stdio.h>
int* f()
{
    //该函数结束时,分配的栈空间会被回收
    int x = 666;
    return &x;
}
int main()
{
    int * p = f();
    //此时程序出错
    printf("%d",*p);
    return 0;
}

七、空指针

空指针的概念

         一个指针不指向任何数据,我们就称之为空指针,空指针用NULL表示

int * p = NULL;

八、指针和数组

指针和数组的关系

数组名本身就是个地址常量, 指针指向时不需要取地址符,直接指向数组名即可

int a[5] = {1,3,5,7,9};
int * p = a;//直接引用即可不需要加取地址符 '&'

 数组名代表数组的首地址,取值之间的语法可以相互套用

int a[5] = {1,3,5,7,9};
int * p = a;
int i = p[1];
int j = *(a+1);
printf("i = %d,j = %d\n",i);

结果: i = 3,j = 3


指针和数组的区别

(1) 赋值方式不同

 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝

(2) 存储方式不同        

数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的, 数组的存储空间,不是在静态区就是在栈上。

指针:指针很灵活,它可以指向任意类型的数据。 指针p存储的是一个内存地址,这个地址指向的是某种类型变量的存储空间。 如果要访问这个变量,需要使用指针运算符(*)来解引用指针,将指针所指向的地址转换为所指向的变量的值。 指针的值也可以改变,通过指针运算符(&)获取变量的地址,然后将其赋给指针变量。

(3) 占用空间大小

数组的大小取决于数组元素的类型和元素个数

数组所占存储空间的内存:sizeof(数组名)

指针无论是什么类型,在32位平台是占4 byte ,在64位平台是占8 byte

(4) 可变性

数组的大小在定义时就已经确定,无法改变,而指针可以随时指向不同的变量,从而实现动态变化。

九、指针数组和数组指针

指针数组

//1.指针数组 是一个数组
int a = 3, b =5;
int * p[2] = {&a,&b};
*p[0] = 15;    //p[0]存储的a的地址

指针数组首先是一个数组,只不过数组的每个成员是一个指针变量。

例:int * p1[10]; // 指针数组,[ ]的优先级大于*,p是一个数组,数组的值是一个指针

数组指针

//2.数组指针 是一个指针
//定义一个二维数组
int arr[3][3] = 
{
    {2,13,4},
    {5,6,7},
    {8,9,10}
};
//定义数组指针 指向二维数组(声明时,括号一定要加!!!)
int (*p2)[3] = arr;

 数组指针首先是一个指针,这个指针指向一个数组(声明数组指针时,括号一定要加!!!)。


十、指针和字符数组

(1) 字符数组

定义方式

//省略{},省略长度值(实际上该数组长度为4 字符串默认'\0'结尾)
char arr[] = "abc";
char s[4] = {'a','b','c','\0'};

输入方式

char s[4];
scanf("%s",s);

字符数组,可以直接用scanf 输入,且不需要加&符,

因为字符数组名,就代表了整个字符串的首地址

(2) 字符指针

定义方式

char  *s = "Hello";

 输入方式

错误写法 X X X 

char *s;//这样写是错误的 !!!!!!!!!
scanf("%s",s);//这样写是错误的 !!!!!!!!!

注意:

这里的字符指针未指向有效数据空间,用scanf()输入程序必然出错!!!

正确写法 √ √ √

char a[100];
char *s = a;//字符指针 s 指向了字符数组 a
scanf("%s",s);

这里字符指针s指向了组a,分配了有效空间,这样才是正确写法,程序正常运行


十一、指针和动态内存 堆和栈

(1)栈(satck): 由系统自动分配。 例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间。

(2)堆(heap): 需程序员自己申请(调用malloc,realloc,calloc),并指明大小, 并由程序员进行释放。容易产生memory leak(内存泄漏).

分配方式

(1)堆都是动态分配的,没有静态分配的堆。

(2)栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由allocal 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现


十二、动态内存函数 malloc calloc relloc 和free的使用

(1) malloc 函数

void * malloc(开辟空间大小) 不会默认初始化,比如开辟空间后,进行调用,会有一些乱码

int n = 5;
int *m = (int*) malloc(n * sizeof(int));

(2) calloc 函数

void * calloc(申请空间的个数,单个类型的大小),默认初始化为0

int n = 5;
int *c = (int*) calloc(n,sizeof(int));

 (3) realloc 函数

void * realloc(p需要调整的指针,新的大小),对于内存开辟空间大小的更改。

(1)改小:

对申请的内存空间改小,可以在原申请处减小可访问字节数,这样就做到了对使用空间的减小。

(2)改大:

1.malloc或者calloc申请得到的空间后面有足够的空供我们使用,直接开辟


2.假设realloc可连续操作的剩余空间够扩大的所需空间,会返回本来的地址 p


2,若所需的空间不够,会将原本申请的空间释放掉(还给操作系统),找一块新地盘,并把上面空间的数据复制到新的空间中,还会把p指针指向的地址改为新申请的地址

int * r = (int*) realloc(c,sizeof(int)*n*2);

注意:新开辟的空间,会有垃圾值的概率,不会进行初始化


十三、内存泄露

内存泄漏的概念

内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(内存空间用完了,没有释放,上完公厕,人不走占着坑)

内存泄漏的危害

内存泄漏会因为减少可用内存的数量,导致降低计算机的性能,甚至程序崩溃。

如何防止内存泄漏

谨慎申请内存,使用后即使对内存释放,free(被释放内存指针)

int * a =(int*)malloc(sizeof(int)*10000);
//堆内存需要手动释放,否则可能会引起内存泄漏
free(a);

十四、函数返回指针

定义写法 类型名 *函数名(参数表列);

有时候我们需要指针作为返回值时,需要申请动态内存,栈内存会在函数结束时销毁

#include <stdio.h>
#include <malloc.h>

int * getPointer()
{
    //这里需要用堆内存,栈内存在函数执行后,销毁
    int * c = (int*) malloc(sizeof(int));
    *c =999;
    //static int c = 999;//静态区也不会销毁
    return c;
}
int main() {
    int * p = getPointer();
    printf("%d",*p);
    free(p);//注意释放内存
    return 0;
}

十五、函数指针

函数指针的概念

当一个指针,指向的对象是函数时,我们称它为函数指针

函数指针的作用

当我们需要,把一个函数当做参数传递时,我们可以利用指针的特性,于是就有了函数指针

#include <stdio.h>

//返回a + b的一个函数
int add(int a,int b)
{
    return a + b;
}

int main() {
    //(*p)括号一定要加!!!
    int (*p)(int,int);
    //add不需要加'&'符,因为函数名本身就代表地址
    p = add;//函数指针p,指向 函数add
    int res = p(3,4);
    printf("res = %d",res);
    return 0;
}

 注意:

如果函数指针指向的函数参数列表为空,例如void test(),这时我们定义函数指针时依然也要加上括号。赋值给函数指针时,函数只给名字! ! !否则编译器无法识别是调用还是赋值

void test()
{
    printf("test~");
}
int main() {
    int (*p)();
    p = test;
    //用函数指针执行函数
    p();    
    return 0;
}

十六、回调函数

回调函数的概念

回调函数是一种编程概念,指的是一个函数作为参数传递给另一个函数

回调函数的作用

(1) 代码逻辑分离

回调函数允许将代码逻辑分离出来,使得代码更加模块化和可维护。

(2) 异步编程

回调函数可以在某个函数执行完后被调用,通过这种方式可以将结果传递到另一个函数中进行处理,起到异步编程的作用。

(3) 代码复用

由于回调函数可以被多个地方调用,它们可以实现代码的复用。

 (4) 事件处理

回调函数可以在发生某种事件时由系统或其他函数自动调用,用于对该事件或条件进行响应

回调函数怎么写

#include <stdio.h>

//加法函数
int add(int a,int b)
{
    return a + b;
}
//减法函数
int sub(int a,int b)
{
    return a - b;
}

//计算器
int cal(int a,int b,int (*f)(int, int))
{
    return f(a,b);
}
int main()
{
    int a = 7, b = 4;
    //把函数 sub,作为参数传入,cal函数只负责返回最终结果
    int res = cal(a,b,sub);
    printf("res = %d",res);
    return 0;
}

代码解析:这里验证前面的理论,我们写了三个函数,add(加法函数),sub(减法函数),cal(计算器),我们cal只负责接收两个整型然后返回计算的值,而怎么计算只需要根据我们传入的函数来决定,增加了代码复用率,更加模块化和可维护。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值