前言
本文主要讲解指针的多种用法,和一些概念,不过多赘述一些函数的用法,全文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只负责接收两个整型然后返回计算的值,而怎么计算只需要根据我们传入的函数来决定,增加了代码复用率,更加模块化和可维护。