内存:程序运行过程中,可以访问到的内存存储空间;计算机为了方便对内存的管理,以字节为单位,对内存进行了编号。
地址:计算机管理内存时,给内存的编号
指针:指针即为地址,地址即为指针,指针指向了一个具体的空间
指针变量:变量,保存地址的变量,需要占用内存空间。
一.指针基础
1.指针变量的定义
基类型 *变量名;
基类型:该指针指向的空间存储的数据的数据类型:int、char、float 、double 、void、构造数据类型
* : 类型说明符,说明后面的变量是一个指针
变量名:遵循标识符的命名规则
p开头
pmax, ptmp.指针变量中保存的地址,是该数据类型的首地址
int *pint; char *pch; int *pi, *pj;
sizeof (指针) 在64位计算机系统下,指针在内存中占用8字节。
在32位计算机系统下,指针在内存中占用4字节。
2.指针变量的初始化和赋值
野指针:初始化或赋值的一个指针,或者指向一个已经释放的内存空间的指针。
避免野指针:
(1)给指针初始化为空指针NULL;
(2)给指针初始化为已有变量的地址。
int *p; // 野指针(里面存了一个随机地址,比如 0x0001) *p = 10; // 试图往那个随机地址里写数据
编译结果:编译绝对不会报错。编译器只检查语法,
*p语法完全正确,编译器认为p是int*,解引用是合法的。运行结果:不一定崩溃,但绝对属于“未定义行为”。
野指针可能指向以下三种区域,结果完全不同:
1. Segmentation fault (core dumped)段错误,访问的地址属于操作系统受保护的区域(比如 0x0000 附近),好排查(立竿见影的崩溃)
2. 未分配但可读写的内存,写入成功,不报错,但悄悄覆盖了其他变量的数据,极难排查(出现随机 Bug)
3. 只读区域(如代码段),崩溃(Bus Error)
3.指针运算符
* ----》指针运算符
用来访问指针变量所指向的内存空间的数据
*指针变量;-----》指针降级为变量类型---》基类型的变量int *p; p; ---->int* *p; ---->int char * pch; pch - --->char注意:
1.* 指针运算符,只能放在指针变量的前边。
2.*指针变量,访问指针变量指向的数据,该表达式降级为变量类型---》基类型的变量
3.*指针变量,访问的字节数取决于定义该指针时的基类型。
char*p; *p--->char ->访问1字节 int * p; * p--->int --->访问4字节 double *p; *p-->double ---->访问8字节
& 取地址运算符
获取某个变量的首地址。
&变量名;
1.&只能给一个变量取地址,不能给常量或者表达式取地址。
2.&给某个变量取地址时,获取的是这个变量首字节的地址。
4.指针的算术运算
+ * / % 对于指针运算无意义。
- : 指针1- 指针2:表示两个指针之间间隔的元素个数(地址值之间的字节差/sizeof (基类型))
++:指针+1:表示从当前地址向后偏移sizeof (基类型)字节
--: 指针-1:表示从当前地址向前偏移sizeof (基类型)字节char*p1; p+1;---->向后偏移1字节 int* p2; p+1;-->向后偏移4字节 printf("%p\n",p1);---->%p打印地址 printf("%ld\n",p1-p2);---->%ld打印两地址之差
二.指针作为函数参数
1.被调函数中,想要修改主调函数中定义的变量,传递该变量的地址;
2.被调函数中,想要使用主调函数中定义的变量,可以传递值,也可以传递地址;3.被调函数想要返回多个结果时,可以传递主调函数定义的变量的地址,地址指向的空间可以保存被调函数得到的结果。
三.指针和一维整型数组
数组的数组名为数组首元素地址,且为地址常量,不可被修改。
注意:
1.数组名为地址常量(右值),不能被赋值
a===>&a[0]
a[0]---->*a
a[1]---->*(a+1)a[2]---->*(a+2)
a[n]---->*(a+n)
int a[]={0};
int num=0;
a=# //×
a++; //×
*a=1; //√ 等价于a[0]=1;

一维整形数组作为函数参数传递:
void print_ array( int a[ ] , int len){ }
void print_ array( int *pa, int len){ }
a与pa用法相同
四.指针和一维字符型数组的关系
数组的数组名为数组的首元素地址,且是一个地址常量。
char str[32]={“hello”};
str -----> & str [0]-----> char * ------->*str------->str[0]str +1-----> & str [1]-----> char * ------->*(str+1)------->str[1]
......
str +n-----> & str [n]-----> char * ------->*(str+n)------->str[n]
str+1:指针向内存高地址偏移sizeof (char)字节数
str- 1:指针向内存低地址偏移sizeof (char)字节数
虽然用字符数组和字符指针变量都能实现字符串的存储和运算,但它们二者之间是有区别的,不应混为一谈,主要有以下几点。
(1)字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第1个字符的地址),决不是将字符串放到字符指针变量中。
(2)赋值方式。对字符数组只能对各个元素赋值,不能用以下办法对字符数组赋值:
char str[14]:
str= I love China!而对字符指针变量,可以采用下面方法赋值:
char*a;
a="I love China! "
但注意赋给a的不是字符,而是字符串第一个元素的地址。(3)对字符指针变量赋初值:
char* a= "I love China"; 等价于 char*a; a= "I love China!";而对数组的初始化:
char str[14] = "I love China! "; 不能等价于 char str[14]; str= "I love China!";即数组可以在定义时整体赋初值,但不能在赋值语句中整体赋值。
五.数组指针
数组指针:本质是指针,指向数组整体的指针。
int (*变量名) [5] ;----》数组指针
定义一个指针变量,指针变量自己占8字节空间,可以指向一个具有5个元素的整形数组。
int a[5] = {1,2,3,4,5};
数据类型
a:首元素地址===》&a[0] ===>int
&a:数组的指针=====>int (*)[5];
&a+1:数组指针+1====》代表偏移整个数组(sizeof(a)字节)

注意:
int a[5] = {1,2,3,4,5};
a:首元素地址---》int*
除了2种场景:
1.sizeof(a/数组名);a不能当作int *类型,当作数组整体,代表整个数组。
2.&a:数组指针,该指针类型为 int (*)[5],指向数组整体。a 是 int*,&a 是 int (*)[5]。两者地址数值相同,但类型和步长截然不同。&a 指向的是那块连续内存的“整体”,而不是指向“指向该内存的指针”。
总结对比表(针对行指针 int (*p)[3])
| 表达式 | 编译器眼中看到的类型 | sizeof 结果 | 含义 |
|---|---|---|---|
p | int (*)[3] | 4/8 字节 | 存放地址的盒子本身大小 |
*p | int [3] | 12 字节 | 一整行的大小(不退化为指针) |
**p | int | 4 字节 | 一个元素的大小 |
总结:sizeof 看的是类型,不是实际内存值。*p 在 sizeof 里是“一整行”,在算术运算(如 *p+1)里才会退化为“指向首个元素的指针”。
六.数组指针和二维数组
二维数组的数组名是首元素地址,首元素是一维数组,所以二维数组数组名本质是一个一维数组指针。
int a[5]={1,2,3,4,5};
a 首元素地址 &a[0] int*
a[n]--->*(a+n)
inta[2][3]={1,2,3,4,5,6};
a 首元素地址 &a[0] int(*)[3];-->数组指针
a [ m] [ n]===>*(a[m]+n)===>*(*(a+m)+n)
二维数组的数组名作为函数参数传递:
int a[2][3]={1,2,3,4,5,6};
void print_2d_array(int a[ ][列数], int hang)
{ }
void print_2d_array(int (*pa)[列数], int hang)
{ }
print_2d_array(a,2);
void print_str_array(char str[ ][128], int hang)
{ }
void print_str_array(char (*pstr)[128], int hang)
{ }
| 表达式 | 原类型 | 原步长(+1) | 指向对象 | 加 * 后(解引用) | 解引用后的类型 | 解引用后的步长 | 解引用后指向/代表 |
|---|---|---|---|---|---|---|---|
a[0] | int * | 4 字节 | 第0行第0个元素 | *a[0] | int(普通值) | 不适用(不是地址) | 取出该位置的数值(如 1) |
&a[0] | int (*)[3] | 12 字节 | 第0行整体 | *(&a[0])等价于 a[0] | int * | 4 字节 | 第0行第0个元素 |
a(退化后) | int (*)[3] | 12 字节 | 第0行整体 | *a等价于 a[0] | int * | 4 字节 | 第0行第0个元素 |
&a | int (*)[2][3] | 24 字节 | 整个二维数组 | *(&a)等价于 a(退化后) | int (*)[3] | 12 字节 | 第0行整体 |
总结:
-
取地址
&是“升维”:增加一层指针包裹,步长变大。 -
解引用
*是“降维”:剥去一层指针包裹,步长变小。 -
规律:
-
&a[0]取*回到了a[0](int*)。 -
&a取*回到了a(int (*)[3])。 -
对
a[0](已是int*)取*,直接得到底层数值(int),不再是地址。
-
对于a[0]与&a[0][0],虽然赋值给指针时两者一样,但在 sizeof 操作中,它们有本质区别:
-
sizeof(a[0]):此时a[0]不再退化,它代表的是整个第一行数组,结果是列数 * sizeof(int)(例如 3*4=12 字节)。 -
sizeof(&a[0][0]):这是取地址,结果永远是指针的大小(4 或 8 字节)。
结论:只要不是作为 sizeof 或 &(取地址)的操作数,a[0] 在绝大多数情况下(比如函数传参、赋值给指针)就等同于 &a[0][0]。
七.指针数组
指针数组:本质是数组,该数组中保存的是指针。(多用于char*指向字符串,整型用的很少)
语法:
int a[5]:
int*a[5];
int *: 数组中保存的数据类型:该数组中保存的元素是int*指针[5]: 该数组中可以保存5个指针(int*)
定义了一个可以保存5个整形指针的数组,整体占40个字节
char*p[5];
1.指针数组的定义
指针类型 数组名 [元素个数];
2.指针数组的初始化
char * p =NULL; char * p[5]={NULL};将5个字符型指针初始化成NULL;
char * p = "hello"; char *p[5] = {"hello", "world","how", "are", "you" } ;p[0]==== > char *====>p[0]指针保存字符串常量"hello"的首地址
p[1]==== > char *====>p[1]指针保存字符串常量"world "的首地址
p[2]==== > char *====>p[2]指针保存字符串常量"how"的首地址
p[3]==== > char *====>p[3]指针保存字符串常量"are"的首地址
p[4]==== > char *====>p[4]指针保存字符串常量"you"的首地址char *p1 ="hello"; char *p2 ="world"; char *p3 ="how"; char *p[3] ={p1,p2,p3};
3.指针数组和二维字符型数组区别
char str[3][100]={"hello","world","how"}; sizeof(str)=300字节char *pstr[3]={"hello","world","how"}; sizeof(pstr) ==3 * sizeof(char * ) == 24字节区别:
1.二维字符型数组中保存的是多个字符串本身;指针数组中保存的是多个字符串的首地址;
2.要保存多个字符串用二维字符型数组,要操作多个字符串用指针。
4.指针数组作为参数传参
void pointer_array_sort(char *pa[],int len)
void pointer_array_sort(char *pa[],int len) //指针数组作为参数传参时,为char**类型,可以用char *pa[]或char**接受 { char *tmp=pa[0]; //两边都为char *类型,若char *tmp=pa则不匹配,pa为char**类型 for(int i=0;i<len-1;i++) { for(int j=0;j<len-i-1;j++) { if(strcmp(pa[j],pa[j+1])>0) { tmp=pa[j]; pa[j]=pa[j+1]; pa[j+1]=tmp; } } } } char * str[]={"hello","world","how","are","you"}; pointer_array_sort(str,len);
八.二级指针
本质还是指针,是一个指针变量的地址。
int ** 指针名称;------》保存整型指针的指针
char ** 指针名称;------》保存字符型指针的指针
C语言中主要有两个二级指针的使用场景:
1.想要在被调函数中修改主调函数中的指针变量,实参需要传递该指针变量的地址;形参用二级指针接收。
2.指针数组的数组名,是一个二级指针,实参传递指针数组的数组名时,形参用二级指针接收

。
九.指针函数
指针函数:本质是函数,具有指针类型返回值的函数成为指针函数。
指针函数能返回什么指针?
1.局部变量的地址不能被返回(局部变量,局部数组)。char str[]={"hello,world"}; //数组在局部空间开辟,函数结束时回收内存 char *str="hello,world"; //字符串常量在常量区,函数结束不会被回收,在调用结束前返回字符串常量地址
2.具有静态生命周期的变量的地址可以被返回。
(1).static 修饰的静态变量地址。
(2).全局变量的地址。
3.可以返回字符串常量的地址。
4.可以返回通过参数传递给该函数的地址。char * strcpy(char *dest,const char *src);5.可以返回堆区申请且没有被释放的空间地址。
十.函数指针
函数指针:本质是指针,指向函数的指针。
函数:函数的函数名,为函数的入口地址。(地址常量,不可被赋值)
函数指针语法:
1.定义函数指针
函数声明:int fun (int,int); int * pfun( int int );函数指针:
int (*pfun) ( int, int);定义了一个函数指针变量,该指针变量在内存占8字节,该函数指针可以指向一个具有int返回值,两个int类型的参数的函数。
char *(*pfun) (char *);总结:
返回值类型 (*指针名称) (形参表);
2.函数指针初始化
int (*pfun)(int,int) =NULL;
3.函数指针赋值
int add(int num1,int num2)
{
return num1+num2;
}
int sub(int num1,int num2)
{
return num1-num2;
}
int (*pfun)(int,int) = NULL;
pfun = add;
int ret=pfun(10,20);
pfun = sub;
int ret1=pfun(10,5);
4.函数指针调用函数
函数名(实参表);
函数指针名称(实参表);
5.函数指针的应用
回调函数:将函数作为某个函数的参数传递,传递的函数称为回调函数。(减少重复代码,降低代码耦合性)




218

被折叠的 条评论
为什么被折叠?



