朝花夕拾 · C语言 | 指针篇

内存:程序运行过程中,可以访问到的内存存储空间;计算机为了方便对内存的管理,以字节为单位,对内存进行了编号。
地址:计算机管理内存时,给内存的编号
指针:指针即为地址,地址即为指针,指针指向了一个具体的空间
指针变量:变量,保存地址的变量,需要占用内存空间。

一.指针基础

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 结果含义
pint (*)[3]4/8 字节存放地址的盒子本身大小
*pint [3]12 字节一整行的大小(不退化为指针
**pint4 字节一个元素的大小

总结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个元素
&aint (*)[2][3]24 字节整个二维数组*(&a)
等价于 a(退化后)
int (*)[3]12 字节第0行整体

总结:

  1. 取地址 & 是“升维”:增加一层指针包裹,步长变大。

  2. 解引用 * 是“降维”:剥去一层指针包裹,步长变小。

  3. 规律

    • &a[0] 取 * 回到了 a[0]int*)。

    • &a 取 * 回到了 aint (*)[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.函数指针的应用

回调函数:将函数作为某个函数的参数传递,传递的函数称为回调函数。(减少重复代码,降低代码耦合性)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值