指针与内存
内存有三个属性:
1.变量名
2.存储的数据
3.地址
假设p的数值是一堆地址,正好是a变量的地址。用*p可以表示a的数值,实际上可以将*p与a等价。
野指针或者内存泄漏,可以理解成为假设有内存块是由指针去定义,指向变量b并且赋值为73,但是指针p又发生了改变,去指向a。此时之前的内存块虽然存放数据,但是无法通过变量或者指针去把数据选出来,因为你永远不知道地址在哪里。
一个变量不知道指向哪里的时候,让它指向为空指针。
int *p = NULL;
一.指针运算
指针+-运算
指针地址是由低到高变化的。
2.指针-指针
指针-指针得到的是两个指针之间的元素个数
指针-指针的前提是两个指针指向同一块空间
3.指针的关系运算
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
二.数组是存放数据长度固定的容器,并且其数据类型也是一致的。
数组名是数组首元素的地址。
三.二维数组
将二维数组arr视为一个矩阵,下图显示了数组中每个元素在矩阵中的存放位置
数组中各个元素在矩阵中对应的位置由二维数组的两个下标决定。我们可以将定义的二维数组int arr[4][3]视为由arr[4]和int [3] 两部分构成,将arr[4]视为一个整型一维数组,其中含有4个元素arr[0]、arr[1]、arr[2]、arr[3],每个元素都是int[3]类型的,也就是说,每个元素又是一个一维数组,每个一维数组含有3个元素,如arr[0]含有arr[0][0]、arr[0][1]、arr[0][2]三个元素。
二维数组的初始化遵循行优先规则:
int ar[3][3]={1,2,3,4,5,6};那么初始化时会把第一行赋值后,在对第二行前两个赋值
但是如果int ar[3][3]={undefined{1},{2,3,4},{5,6}};这样的写法就会依照一个括号代表一行来初始化
还需要注意的是二维数组初始化时,行的个数可以省略,而列的个数不能省略,这是因为在实际内存中二维数组是在连续空间存放的,如果不给出列的个数,那么就无法确认每一行的元素个数。
四.二级指针
格式:类型 **
int ** //指向整型的指针的指针类型
char ** //指向字符型的指针的指针类型
float ** //指向浮点型的指针的指针类型
int a = 10;
int b = 20;
int c = 30;
int ar[3] = { &a,&b,&c }; //错误,不可以将几个地址元素赋给一个整型数组
int *br[3] = { &a,&b,&c }; //正确,将几个地址元素赋给一个整型指针数组
五.指针类型
结构体指针:把一些相关的变量组合起来,以一个整体形式对对象进行描述。类如一位学生的信息管理,他可能有,姓名(char),学号(int)成绩(float)等多种数据。
结构体小知识:
1.只有结构体变量才分配地址,而结构体的定义是不分配空间的。
2.结构体变量的声明需要在主函数之上或者主函数中声明,如果在主函数之下则会报错。
3.c语言中的结构体不能直接进行强制转换,只有结构体指针才能进行强制转换。
4.相同类型的成员是可以定义在同一类型下的
struct Student
{
int number,age;//int型学号和年龄
char name[20],sex;//char类型姓名和性别
float score;
};
指针数组:指针数组可以说成是”指针的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。
数组指针:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。
指针函数: 顾名思义,它的本质是一个函数,不过它的返回值是一个指针。其声明的形式如下所示:
其中,func
是一个函数,args是形参列表,ret *
作为一个整体,是 func
函数的返回值,是一个指针的形式。
六.指针和函数
(1)指针传参
对于普通的参数传递,传入的值是什么,调用完函数之后,值不发生变化。我们无法通过调用函数改变值,但是使用指针传递时,可以达到改变值的目的,因为指针传递的是变量的地址,不是值。
1.使用指针作为参数,可以实现两种功能:
(1)可以读取上一层函数中的变量的值*p
(2)可以修改上一层函数中变量中的值*p(普通参数无法实现)
2.使用指针交换两个参数
(2)函数指针
指向函数的指针变量,是一个指向函数的指针
例如:int *p(int a)是一个函数指针
回调函数:
指一个通过函数指针调用的函数。如果把函数的指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就称其为回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的。
例如:qsort()函数,对应的头文件为#include <stdlib.h>
函数也可以作为函数的参数来传递
首先至少要有 3 种类型的函数
-
主函数:相当于整个程序的引擎,调度各个函数按序执行
-
回调函数:一个独立的功能函数,如写文件函数
-
中间函数:一个介于主函数和回调函数之间的函数,登记回调函数,通知主函数,起到一个桥梁的作用
functionB(){
printf("i am callback");
}
functionA(paramA){
printf("I will call the callback next");
paramA();
}
functionA(functionB);
可以看到调用functionA时传入functionB, 而在functionA中 调用functionB,所以functionB被称为回调函数。
(3)指针函数
定义
指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。
声明格式为:*类型标识符 函数名(参数表)
看看下面这个函数声明:
int fun(int x,int y);
这种函数应该都很熟悉,其实就是一个函数,然后返回值是一个 int 类型,是一个数值。
指针函数的写法
int *fun(int x,int y);
int * fun(int x,int y);
int* fun(int x,int y);
(4)结构体
结构体定义的几种方法
#include <stdio.h>
#include <string.h>
#include <malloc.h>
/*先定义几类结构体*/
struct str{
int a;
int b;
}str1;
/*这是第一类结构体的形式,有结构体名,有结构体变量*/
struct str2{
int a;
int b;
};
/* 这是第二类结构体的形式,有结构体名,无结构体变量 */
typedef struct {
int a;
int b;
}STR3;
/* 这是第三类结构体的形式,把结构体重定义为STR3 */
typedef struct str4{
int a;
int b;
}STR4;
/*这是第四类结构体的形式,把结构体重定义为STR4,还有结构体名称*/
七.指针的应用
①函数之间无法通过传参共享变量。属于调用者,函数之间的名字空间相互独立是可以重名的,函数之间的数据传递都是值传递。
②堆内存无法与标识符建立联系,只能配合指针使用。
类型一
#include<stdio.h>
void func(int a,int b)
{
a=100;b=50;
}
int main()
{
int a=50;int b=100;
func(a,b);
printf("%d %d",a,b);
}
结果 50 100
在以上的代码块中函数之间就无法共享变量(main的a.b和func的a.b是相互独立的)它们变量存放的内存位置并不相同。
#include<stdio.h>
void func(int* a,int* b)
{
*a=100;*b=50;
}
int main()
{
int a=50,b=100;
func(&a,&b);
printf("%d %d",a,b);
}
结果 100 50
以上代码块中传递的是a.b的地址,再对a.b进行解引用修改其中的值。
类型二
int *p =NULL;
p = malloc(4);
与堆内存的配合使用。
指针的易错点
1.内存泄漏:申请的堆内存没有释放。
2.内存污染:前面非法操作使用内存(没有报错),后面写着写着就出错。
3.对空字符串和非法字符串的判断:
图中画蓝线的部分:应该判断的是指针变量的值,而不是指针指向的内存
4.指针越界:如:str[3] = "abc";
5.指针的叠加会不断改变指针的指向。
如:char *p ="sdfg"; p++; printf("%s\n",p);打印结果为:"dfg";。指针的只想被改变,如果再叠加4次,就打印不出内容了,因为指针此时已经指向了结束符。