C语言——7-指针

0 变量的问题

int a = 5;

属性 :
	变量名
 变量的存储单元 : 系统会为每个存储单元分配一个唯一的编号,这个编号就是变量的地址.
 变量的值 : 变量存储单元中的内容.
     
访问变量 :
	a = 1024;//把数值1024存储到  变量a的地址中去
	b = a;// 把变量a的值  赋值给b

==>
 在C语言中,任何一个变量,都有两个意思 :
		(1) : 变量的左值 : 变量的地址
     (2) : 变量的右值 : 变量的值
         
 对变量的访问 :
		read 
         读取变量的值, 从  变量的地址中  取值
     write
         赋值
         把一个数值写到  变量的地址中去.
         
通过上面的分析, 是不是  只要知道变量的地址  就可以读取/写入 变量了呢?
         yes
         
通过变量的地址去访问变量 :
		指针

1 变量的访问方式

  • (1) 直接访问

    通过变量名去访问变量.

    "直接访问的方式" 受作用域的限制.

  • (2) 间接访问

    通过变量的地址去访问变量."指针"

    "不受作用域的限制" :只需要知道你的地址,并且这个地址是可以访问的.

    那么你就可以通过地址去访问他.

2 什么是指针呢?

在程序的运行期间,系统会为每个对象在存储器中, 分配一段内存空间.

存储单元 , 按字节大小给每个存储单元分配一个唯一的编号,这个编号

称为存储单元的地址.

指针的概念和地址差不多,一个对象的地址,也可以称为一个对象的指针.

每一个对象都会有一个地址, 或 指针.

地址就是一个编号,这个编号是非负整数.

3 取地址的运算符

&  单目运算符
 
 &x ---> 取对象x的地址
 这个表达式的值就是 "对象x的地址(整数形式的值, 非负数)"
 
 
int a = 5;
int b = 5;

printf("%p\n",&a);//%p 打印对象的地址, 十六进制输出
printf("%p\n",&b);

scnaf("%d",&a);

4 指针运算符

* 单目运算符, 接一个"地址"
 *地址  <==> 地址对应的那个对象

例子 :

int a =5;

*(&a) : * 后面加一个 "地址" : 变量a的地址
*(&a) ==> 在任何时候,都等价于  地址对应的那个对象 a
 
*&a = 1024;// a = 1024
*&  可以直接约掉
 *&a ==> a
 &*&a ==> &a

注意

*地址 等价于 地址对应的那个对象.

不是对应的只是变量的值,也不是对应的只是变量的地址.

因为那个对象,在不同的上下文, 有可能是左值,也有可能是右值.

#include <stdio.h>

int main()
{
 int a = 5;
 int b;

 // *&a = 1024;// ==> a = 1024
 // printf("%d\n",a);
 // printf("%d\n",*&a);

 b = *&a;// ==> b = a;
 printf("b = %d\n",b);


 return 0;
}

5 指针变量

指针变量是什么? B

A 指针 B 变量

指针变量就是一个变量, 只不过这个变量保存的是另一个对象的地址(指针).

指针变量和普通变量有一点不一样 : 这个东西保存的是另外一个对象的地址.

指针变量如何定义呢?

普通变量的定义 :
	变量的类型 变量名;
	如 :
		int a;

"指向"

如果 p 保存了a的地址,那么我们说 : p 指向了a

保存了谁的地址就指向谁

例子 :

int a =5;

p = &a;//把a的地址, 写入到变量p的存储空间中去
		//p 保存的是  a的地址

	p 指向 a
p是一个指针变量
     
p指向的类型是什么?
     p指向的类型是指 :  p指向的对象的类型
         type(a1) : int

指针变量的定义 :

指向的类型  *指针变量名;

int a;

int *p;//定义了一个指针变量p,指向一个 int类型的变量

p = &a;

//int *p = &a; //指针变量定义时就赋初值

p 是一个变量,C语言中, 任何变量都有左值和右值 :
	请区分 :
		p的左值是什么?   p本身的地址, &p
     p的右值是什么?   p存储空间的内容 , &a
         
     p = xxx;//p代表的是左值
		y = p;//p代表的是p的右值

		*p  : p代表的是p的左值,还是 p的右值呢?
         	p的右值
         

指针变量的使用 :

p = &a;//把 a 的地址,存入到 p的存储空间中.
	
	p 指向 a
 p的左值 是p本身的地址 &p
 p的右值 p存储单元的内容, &a
     *p ==> *&a ==> a
     *p   p指向的对象

例子 :

int a = 5;
int *p;
p = &a;

*p = 250;
a = 1024;

#include <stdio.h>

int main()
{
 int a = 5;
 int *p;
 p = &a;
     //p的右值 是 p存储单元的内容,&a
     //p的左值 是 p本身的地址
 // int *p = &a;//定义了一个指针变量p,同时初始化p
 //         // ==> int *p; p = &a;

 // printf("%p\n",p);
 // printf("%p\n",&a);
 // printf("%p\n",&p);

    *  指针运算符, 后面可以接一个地址, * 地址,代表是地址对象的那个对象
    * &a = a;
 
 *a  a此时是a的右值, a的右值是一个整数值,不是地址.  *a是有问题的.

 //*p : p代表的是p的右值, p的右值是一个地址
 // *p ==> * &a ==> a
 *p = 1024;//a = 1024
 int b = *p;
 printf("a = %d\n",a);
 printf("b = %d\n",b);


 return 0;
}

6 野指针 与 空指针

6.1 野指针

野指针是一个指向未知(undefine)的 , 不确定地方的指针.

"未知的,不确定的" , 指向的地方可能存在,可能不存在.

可能可以访问,也可能不可以访问.

对野指针的访问,会有后果?

可能可以访问,可能不可以访问(导致非法的内存访问).

非法的内存访问

不存在的地方,你去访问

存在但不能写,你去写

存在但不能读,你去读

后果 : 非法的内存访问,会导致 "segmentation fault段错误" , 系统把你的进程给kill掉.

例子 :

1. int *p;
	//定义了一个指针变量p , 你没有赋初始值,不代表p没有值,相反p一定会有一个值.
	//意思是p一定会指向一个地方,但是这个值是多少,指向哪里,
	//你是不知道的  undefine(未知的,未定义的)

请问  p 是不是一个野指针? A
 A 是  B 不是
 
2. int *p = 5;
		//p是一个指针  指向一个地址为5的地方,p是一个指向确定的地方
		//p不是野指针, 尽管 5 这个地址可能不存在
	
	请问  p 是不是一个野指针? B
 A 是  B 不是
     
3.  int *p;
	int a;
	...
 p = &a;

	请问  p 是不是一个野指针? 
 A 是  B 不是

6.2 空指针

空指针是一个指向空(不存在的地方,NULL)的指针.

空指针不是野指针,因为空指针指向了一个 确定的地方(尽管这个地方不存在).

对空指针的访问,一定会导致 "非法的内存访问(段错误)"

int *p = NULL;//p 是空指针

*p  //用*p 就会出现段错误.

注意

不能对野指针进行操作, 这种行为是很危险. 宁可使用空指针,也不可使用野指针.

在定义指针的时候,可以让指针指向空的地方.

接下来, 我们来看下面的代码 :

int a;
int *p = &a;
	//指针p, 指向a
	//假设 &a : 0x3000
printf("%p\n",p);//0x3000
printf("%p\n",p+1);//0x3001?

7 指针做加减

a + b
a - b

加号+ 减号- 只要求 a ,b 是一个数(整数,浮点数)

a,b可以是一个指针,指针的值本质是一个地址编号(非负整数)

指针是可以加减的, 指针加减一个整数, 表达式的类型还是原指针的类型.

例子 :

int a;
int *p;//p 是一个指向 int 类型的指针
		//typeof(p) : int *

typeof(a) : int
typeof(p) : int *
typeof(*p) : int
 p是一个指针, *p 表示p指向的那个对象
 typeof(*p) : p指向的那个对象的类型
 
typeof(p) : int*
typeof(p + 1) : int *
typeof(p - 1) : int *
 
double *p2;

typeof(p2) : double*
typeof(p2 + 1) : double*

#include <stdio.h>

int main()
{
 // int a;
 // int *p = &a;
 //     //指针p ,指向a
 //     //假设&a : 0x3000
 // printf("&a = %p\n",&a);//0x3000
 // printf("p = %p\n",p);//0x3000
 // printf("p + 1 = %p\n",p + 1);//0x3004

 double b;
 double* p = &b;
 printf("&b = %p\n",&b);//0x3000
 printf("p = %p\n",p);//0x3000
 printf("p + 1 = %p\n",p + 1);//0x3008

 return 0;
}

指针作加减 p + i(p 是一个指针, i 是一个整数)

不是简单的加减数值,而是 加减i个指向单元的长度

p + i的值是在p的值后面, 挪 i 个指向类型(typeof(*p)) 的长度

p 指向一个int
p + 1 指向下一个int
...
p指向一个double
p + 1 指向下一个double

练习 :

(1) 分析如下程序的输出结果

int main()
{
 double a;
 int *p;
 
 p = (int*)&a;
 printf("p = %p\n",p);
 printf("p + 1 = %p\n",p + 1);
 
 return 0;
}

思考 : 两个指针可不可以做加减? 同类型指针

int *p;
int *q;
p - q 
p + q ? //没有意义

8 数组元素的地址

int a[10];//系统会为a开辟一段连续的内存空间,一次存放数组中的每个元素

	a[0]	    a[1]         a[2]     ...
|-----------|-----------|------------|...
0x3000      0x3004      0x3008        ...
     
&a[0] : 元素a[0]的地址 : 0x3000
     typeof(&a[0]) : int *
     
&a[0] + 1 : &a[1] : 0x3004
     &a[0] 的值是 : 0x3000
     typeof(&a[0]) : int *
     &a[0] + 1 ==> 指针 + 1 ,往后挪一个int
     
&a[0] + 2 : &a[2]
...
&a[0] + i : &a[i]
     
...
&a[3] + 2 : &a[5]
...
&a[x] + y : &a[x + y]
...
&a[0] + i  ==> a[i]
     
*&a[0] ==> a[0]
     
&a[x] + y ==> &a[x+y]
     
*(&a[x] + y) ==> *(&a[x+y]) ==> a[x+y]

练习 :

(1) 分析如下程序的输出结果

int a[10] = {0,1,2,3,4,5,6,7,8,9};
int *p;

p = &a[3];

printf("%d\n",*p);//3
printf("%d\n",*(p + 3);//6
printf("%d\n",*(&a[3] + 3));//6

#include <stdio.h>

int main()
{
 int a[10] = {0,1,2,3,4,5,6,7,8,9};
 
 *(&a[3] + 2 ) = 250;//*&[5] = a[5]

 printf("%d\n",a[5]);
 printf("%d\n",*(&a[0] + 5));
 printf("%d\n",*(&a[4] + 1));

 for(int i = 0;i < 10;i++)
 {
     printf("%p\n",&a[i]);
 }
 printf("-----------------------\n");

 for(int i = 0;i < 10;i++)
 {
     printf("%p\n",&a[0] + i);
 }


 return 0;
}

9 数组与指针

数组名可以当指针来用的

结论

数组名可以当指针(指针常量)来用

数组名当指针用时,数组名可以看作是指向数组第一个元素的指针常量.

如 :

​ 已知 a 是一个数组名(任何类型的数组,任何维度的数组,都可以),

a 当指针用时, &a[0]

数组名即可以代表整个数组,又可以当指针用,

那他什么时候代表整个数组,什么时候当指针用呢?

假设 a 是数组名

  • (1) 在如下情况下 , a代表整个数组
sizeof(a) : 求数组a 的所占的字节数,此时a 代表整个数组
typeof(a) : 求 a 的类型时, 此时 a 代表整个数组.
&a :  a的地址,此时a代表整个数组, 整个数组的地址.

  • (2) 其他情况下, 当指针用
p = a;//a 当指针用,why?  不能把整个数组赋值给其他对象
	 //a 当作指针

	p = a; ==> p = &a[0]

a + 1;//a当作指针用,why? 整个数组不能整体 +1
	a + 1 ==> &a[0] + 1 ==> &a[1]
...         

例子 :

int a[10];

a + 1 //在这个表达式中 , 数组名 a 当作指针来用
 	//a + 1 ==> &a[0] + 1 ==> &a[1]
 
a + i //
 	//a + i ==> &a[0] + i ==> &a[i]
 
*(a + i) ==> *(&a[0] + i) ==> *&a[i] ==> a[i]

结论

*(p + i)  <===> p[i] ,when i >= 0

练习 :

1.分析如下程序的输出结果

int a[10] = {1,2,3,4,5,6,7,8,9,10};

int *p = a + 5;

printf("%d\n",p[2]);//8

  1. 分析如下程序的输出结果
#include <stdio.h>

int main()
{
 int a[10] = {0,1,2,3,4,5,6,7,8,9};
 
 // printf("%ld\n",sizeof(a));//sizeof(a)  a此时代表整个数组
 // 						//sizeof(a) 求整个数组所占的字节数
	// printf("%p\n",a);//此时a当做指针来用, a ==> &a[0]
 // printf("%p\n",&a[0]);
 // printf("%d\n",*a);// a[0]
 
 printf("%d\n",*(a + 1));//
 				//*(&a[0] + 1) ==> *(&a[1]) ==> a[1]
 
 return 0;
}

10 多维数组与指针

预备知识 :

数组名可以当指针用, a , 当指针用 ,&a[0]

C语言中,只有一维数组, 二维/三维/...其实都是一维数组

int a[3][4];
	数组a有三个元素 :
		a[0] ___ ___ ___ ___
     a[1] ___ ___ ___ ___
     a[2] ___ ___ ___ ___
         a[0] a[1] a[2] 又是一个数组
         
*( (a + 1 ) + 1 )
   此处数组名a ,当指针用
*( (a + 1 ) + 1 ) ==> *( (&a[0] + 1) + 1)
               ==> *(&a[1] + 1)
               ==> *(&a[2])
               ==> a[2]
         
*( *(a + 1) + 2) ==> *( *(&a[0] + 1) + 2)
         	 ==> *(*&a[1] + 2)
              ==> *( a[1] + 2)
              ==> *(&a[1][0] + 2)
              ==> *(&a[1][2])
              ==> a[1][2]
         
*(a[1] + 2 )
         
       ===> a[1][2]
---------------
a[0] + 1
   此处数组名a[0] ,当指针用
   a[0] + 1 ==> &a[0][0] + 1 ==> &a[0][1]
---------------
a + 1
 此处数组名a  当指针用
     a + 1 ==> &a[0] + 1 ==> &a[1]
         
&a[1]  
  与  &a[1][0] 仅数值相同, 含义和类型完全不一样的
         typeof(&a[1]) : typeof(a[1]) * ==> int[4] *
         typeof(&a[1][0]) : typeof(a[1][0]) * ==> int*
         
---------------
a
(1) 代表整个数组  sizeof(a),typeof(a) , &a
(2) 当指针用 ,代表首元素的地址, &a[0]
        
a[0] 
 (1) 代表整个数组
 (2) 当指针用,代表首元素的地址,&a[0][0]
         
&a[0][0]
	代表第0行第0列的那个元素的地址
 typeof(&a[0][0]) : typeof(a[0][0]) * ==> int*
         
&a[0]
  代表第0行个那个数组的地址
    typeof(&a[0]) : typeof(a[0]) * ==> int[4]*
         
&a
 代表的整个数组a的地址
         typeof(&a) : typeof(a) * ==> int[4][3] *        

int b[4] ;//b 是一个含有4个int类型的元素的数组
		//typeof(b) : int[4]
		// 元素类型 [元素个数]
int a[3][4];//a是一个含有3个元素, 且每个元素又是4个int类型的数组  的数组.
		typeof(a) : int[4] [3]
      //元素类型 [元素个数]
  

int a[4];

//定义一个指针p,指向数组a

___int (*p)[4]___;定义p   int[4] *p
p = &a;

11 数组指针 与 指针数组

11.1 数组指针

数组指针是什么? B

A 数组 B 指针

数组指针就是一个指针, 指向一个数组的指针.

函数指针就是一个指针,指向一个函数的指针.

数组指针是一个指向数组的指针

int a[4];

//定义一个指针p, 指向数组a
typeof(a) * p;//定义p
	typeof(a) * p; ==> int[4] *p;==> int (*p)[4] 
     
int (*p)[4];//int[4] *p
p = &a;

11.2 指针数组

指针数组是一个数组,里面的元素是指针类型

如 :

int* p[4];//p是一个数组,里面有4个元素,元素的类型是int*

	数组定义格式:
		元素类型  数组名[元素个数]
      int*     p    [4]
         
int (*p)[4];//p是一个指针,指向一个数组

image-20230711171846859

作业 :

  1. 分析如下程序的输出结果

    int a[4] = {1,2,3,4};
    
    int* p[4];
    
    for(int i = 0;i < 4;i++)
    {
        p[i] = a + i;
    }
    
    for(int i = 0;i < 4;i++)
    {
        printf("%d ",*p[i]);
    }
    printf("\n");//1 2 3 4
    
    

    image-20230712092954392

  2. 分析如下程序的输出结果

    int a[3][4] = {
        	1,2,3,4,
        	5,6,7,8,
        	9,10,11,12
    };
    
    int (*p)[4];
    
    p = a;
    
    printf("%d\n",p[1][2]);//7
         
    
    
    
  3. 分析如下程序的输出结果

    (1) 
    	int a[12] = {1,2,3,4,5,6,7,8,9,10,11,12};
    	printf("%p %p %p\n",a,a + 1,&a + 1);
    
    (2) 
    	int b[3][4] = {
        	1,2,3,4,
        	5,6,7,8,
        	9,10,11,12
    };
    printf("%p %p %p %p\n",b,b + 1,&b + 1,b[1] + 1);
    
    (3) 
    	int a[5] = {1,2,3,4,5};
    	int *ptr;
    	ptr = (int*)(&a + 1);
    	printf("%d\n",*(ptr - 1));//5
    
    (4) 
    	int a[5] = {1,2,3,4,5};
    	int *ptr;
    	ptr = (int*)&a + 1;
    	printf("%d\n",*(ptr - 1));//1
    
    

    image-20230712095023046

    image-20230712095509624

有一个问题, 需要各位高手来解决一下 :

int main()
{
 int a = 250,b = 360;
 
 swap(...);//调用swap函数
 
 printf("a = %d,b = %d\n",a,b);//a = 360,b = 250
     
}

//设计一个函数swap 来交换 main 中 a 和 b的值
... swap(...)
{
 
}

我有一个设想 :

int main()
{
 int a = 250,b = 360;
 
 swap(...);//调用swap函数
 
 printf("a = %d,b = %d\n",a,b);//a = 360,b = 250
     
}

//设计一个函数swap 来交换 main 中 a 和 b的值
void swap(int a,int b)
{
 int t;
 t = a;
 a = b;
 b = t;
}

关于函数调用 :

(1) 实参和形参占用不同的存储空间;

(2) 在调用时,是把实参的值 赋值 给形参;

​ "值传递" : 相应的形参 = 实参的值

12 指针作函数的参数

函数的形式参数是一个指针类型.

void func(int *p)
{
 *p = 251;
}

int main()
{
 int a = 250;
 
 func(a);//有问题
 		//p = a;
 		//typeof(a) : int
 		//typeof(p) : int *
 		//类型不匹配
 func(&a);//可以的
 	//typeof(&a) : typeof(a) * ==> int*
 
 a 的值  就变成了 251
 
 return 0;
}

通过传递指针,来修改(或访问)函数外部变量或对象

练习 :

重新设计一个swap 函数, 使得他可以交换两个外部变量的值.

#include <stdio.h>

void swap(int a,int b);

//设计一个函数swap 来交换 main 中 a 和 b的值
void swap_v2(int* p,int* q)
{
 int t;
 t = *p;
 *p = *q;
 *q = t;
}

int main()
{
 int a = 250,b = 360;
 
 //swap(a,b);//调用swap函数  
 swap_v2(&a,&b);//调用swap函数
 
 printf("a = %d,b = %d\n",a,b);//a = 360,b = 250
     
}

//设计一个函数swap 来交换 main 中 a 和 b的值
void swap(int a,int b)
{
 int t;
 t = a;
 a = b;
 b = t;
}

13 数组作为函数的参数

数组作为函数的参数 :

数组元素的类型 数组名[],数组元素的个数

int a[10];

#include <stdio.h>

/*
	find_max : 找一个一维数组的最大值,只求一维数组 int类型元素的数组的最大值
	@a : 元素类型为int 的数组名
	@n : 元素的个数,求 n 个元素中的最大值
	返回值 :
		返回最大值
*/
int find_max(int b[],int n)
{
  //b[0] b[1] ... b[n-1]
  int max = b[0];
  for(int i = 1;i < n;i++)
  {
      if(b[i] > max)
      {
          max = b[i];
      }
  }

  return max;
}

int main()
{
  int a[10] = {1,2,3,4,250,6,7,8,9,10};

  int max;
  max = find_max(a,10);

  printf("%d\n",find_max(a,10));
  printf("%d\n",max);

  return 0;
}

分析一下 :

int find_max(int b[],int n);

在接收实际参数 数组a时 :
	b = a;//此处数组名a  当指针用
==>
 b = &a[0];//typeof(&a[0]) : typeof(a[0]) * ==> int *

函数的形式参数 :
	int b[] ==> int *b //b就是一个指针
     
int find_max(int *b,int n);


结论 :

数组名当函数参数时,就是当指针用

练习 :

1.写一个函数,求一个二维数组的最大值

//求二位数组的最大值
//数组作函数的参数时 :
// 元素的类型 数组名[],元素个数
// int[5]    b[]     int n
//==> int b[][5] ,int n
int find_max(int b[][5],int n)
{
 int max = b[0][0];
 int i,j;
 for(i = 0;i < n;i++)
 {
     for(j = 0;j < 5;j++)
     {
         if(b[i][j] > max)
         {
             max = b[i][j];
         }
     }
 }
     
 return max;
}

int main()
{
 int a[3][5] = {
     1,2,3,4,5,
     6,7,8,9,10,
     11,12,13,14,15
 };
 
 printf("%d\n",find_max(a,3));
 
 return 0;
}

int find_max(int b[][5],int n);

find_max(a,3)
 b = a 两个数组名,此处是当指针用
 b = &a[0]
 
 typeof(&a[0]) : typeof(a[0]) * ==> int[5] *
 
 b 的类型就是 : int[5] * b ==> int (*b)[5]
 

思考 :

int a[3][5] = {
     1,2,3,4,5,
     6,7,8,9,10,
     11,12,13,14,15
 };
和 int a[15] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
从底层存储器的角度分析, 上面两种定义方式有什么区别?

#include <stdio.h>

//求二位数组的最大值
//数组作函数的参数时 :
// 元素的类型 数组名[],元素个数
// int[5]    b[]     int n
//==> int b[][5] ,int n
//int find_max(int b[][5],int n)
int find_max(int (*b)[5],int n)
{
 int max = b[0][0];
 int i,j;
 for(i = 0;i < n;i++)
 {
     for(j = 0;j < 5;j++)
     {
         if(b[i][j] > max)
         {
             max = b[i][j];
         }
     }
 }
     
 return max;
}

int find_max_v2(int *b,int n )//int find_max_v2(int b[],int n )
{
 int max = b[0];
 for(int i = 0;i < n;i++)
 {
     if(b[i] > max)
     {
         max = b[i];
     }
 }

 return max;
}

int main()
{
 int a[3][5] = {
     1,2,3,4,520,
     6,7,8,999,10,
     11,12,13,14,15
 };
 
 //printf("%d\n",find_max(a,3));
 printf("%d\n",find_max_v2(&a[0][0],15));
 
 return 0;
}

回顾一下 :
	有地址的对象,我们可以定义一个指针变量,来保存他们的地址.
     如 :
			int a = 5;
			int *pa = &a;

			int b[4];
			int (*pb)[4];//pb数组指针 指向 int[4]
         pb = &b;
			...
 函数也有地址,我们也可以定义一个指针变量,来保存函数的地址,这类指针 
             称之为 : 函数指针

14 函数指针

14.1 什么是函数指针?

函数指针就是一个指针, 这个指针指向了一个函数(保存了一个函数的地址).

14.2 函数指针如何定义呢?

指针变量的定义格式为 :

指向的类型 *指针变量名;

函数的类型在C语言中如何描述呢?

int sum(int a,int b)
{
 
}
//sum 是一个带两个 int 类型的参数 且返回值为int类型的函数.

void abc(float a)
{
 
}
//abc是一个带 一个float类型参数且无返回值的函数

int find_max(int *b,int n)
{
 
}
//find_max是一个第一个参数为int *,第二个参数为 int ,返回值为 int的函数
//...

描述函数类型 :

返回值类型 (参数类型列表)

typeof(sum) : int (int,int)
typeof(abc) : void (float)
typeof(find_max) : int (int *,int)

定义一个函数指针变量p,保存函数sum的地址,如何定义呢?

typeof(sum) *p;
==>
 int (int,int) *p;
==> int (*p)(int,int);
		//定义了一个函数指针变量p
		//p指向一个函数,指向的类型为 : int (int,int)

在C语言中,定义函数指针变量的格式如下 :

指向函数的返回值类型 (*函数指针变量名)(指向函数的参数的类型列表);

14.3 如何获取一个函数的地址呢?

& 取地址符

如 :

&函数名  --> 某某函数的地址
&sum
&find_max

同时,在C语言中,函数名本身就是一个地址,函数名代表的是函数的首地址.

&sum <==> sum
&find_max <==> find_max
...
p = &sum  <==> p = sum

14.4 如何通过函数指针去调用函数呢?

p = &find_max;
*p = *&find_max ==> find_max

通过函数指针去调用他指向的函数的语法如下 :

  • (1) (*函数指针变量)(实参表达式列表)
  • (2) 函数指针变量(实参表达式列表)

练习 :

1.写一个函数,返回一个整型数组的最大元素的地址,并且通过函数指针去调用这个函数.

(1) 明确任务目标
 "求数组最大值元素的那个地址"
 find_max_addr
(2) 确定输入参数
 "输入一个数组" : 数组的首地址  和 元素个数
(3) 确定返回值
	返回 数组中最大值元素的地址  int*
	
	/*
		find_max_addr : 返回一个整型数组最大值元素的地址
		@a : 指针, 数组的首地址
		@n : 数组元素个数
		返回值 :
			返回 数组中最大值元素的地址
	*/
	int* find_max_addr(int *a,int n)
	{
		//int max = a[0];//max保存最大值
		int *p = &a[0];
		for(int i = 0;i < n;i++)
		{
			if(a[i] > *p )
			{
				//max = a[i];
				p = &a[i];
			}
		}
		
		return p;
	}

	
	int main()
	{
		int a[5] = {1,2,5,4,3};
		int*  (*p)(int *,int);
		p = find_max_addr;
		printf("%p\n",p(a,5));
		printf("%p\n",&a[2]);
		
		return 0;
	}

14.5 函数指针有什么用?

函数指针是通过 指针 去调用他指向的函数.

为什么不直接用函数名去调用呢?

直接用函数名调用不是更简单!!!

那么到底为什么要用函数指针呢?

call me back

callback 回调

函数指针主要是为了实现回调 : 现在不调用, 回过头调用.

思考 :

int *p(int,int);

int (*p)(int ,int);

	上面两个p是一样的吗?
     不一样的
 第一个p : 函数声明 , p是一个函数名,返回值为 int*
 第二个p : 函数指针变量p的定义, p是一个函数指针变量名,指向的函数 返回值为int,带两个int的

15 二级指针与多级指针(就是一个指针)

image-20230712160759361

int a = 5;

//定义一个指针变量p ,来保存a的地址
int *p = &a;

//可以定义一个指针变量p2,来保存p的地址
int* *p2 = &p;

//可以定义一个指针变量 p3,来保存p2的地址
int** *p3 = &p2;
...
 
//*a  不对的, *指针运算符, 他后面需要接一个地址   
 
*p  //p的右值, &a
 *p ==> *&a ==> a

//**p   **p ==> **&a ==> *a 不可以的
 
**p2  //p2 的右值 &p
 **p2 ==> **&p ==> *p ==> *&a ==> a
 
***p3 //p3的右值 &p2
 ***p3 ==> ***&p2 ==> **p2 ==>**&p ==> *p ==> *&a ==> a
 
**p3 //p3的右值  &p2
 **p3 ==> **&p2 ==> *p2 ==> *&p ==> p
 
****p3 ==> ... ==> *a
 把a的值当作一个地址  a的值不一定是一个地址,有问题
 *a *后面要接一个地址.不可以
 
 
  

#include <stdio.h>

int main()
{
 int a = 5;
 int b = 250;

 int *p = &a;
 int** p2 = &p;
 int*** p3 = &p2;

 *p2 = &b;
 
 printf("%p\n",*p2);//*p2 ==> *&p ==>p
 printf("%d\n",**p2);
 printf("%p\n",&a);



 return 0;
}

注意

if x 是一个指针 , x 和 *x 分别代表什么?

x 代表本身

*x 代表x指向的对象.

接下来 ,我们在看下面这段代码

int n;
scanf("%d",&n);//输入数组元素的个数

int a[n];//这个在标准C里面是不允许的,定义数组时,元素个数一定要确定好,
		//C语言只支持 "静态数组"

//有没有办法实现 "动态数组"?
//动态数组 : 在程序运行期间,根据用户输入的值,来确定数组的大小.

16 动态内存分配的函数

"动态内存" : 在程序运行期间, 动态分配内存空间, 一般是在 "堆,heap"空间上分配

malloc/realloc/calloc/free

malloc : memory allocate 内存分配

realloc : repeat allocate 再分配

​ 重新分配 : 一次内存分配完成后,后面用的时候,发现不够用,再分配.

calloc : clear allocate 分配内存时同时清 0

free : 释放

​ 动态分配的内存,需要在你不用的时候,要手动释放free, 否则,这个空间一直是你的,

动态内存分配(malloc/realloc/calloc) 如果分配了空间,一直存在,直到你手动free或进程结束.

有时候,把动态分配的内存"匿名内存" , 匿名内存只能通过他的指针去访问.

image-20230712170956322

16.1 malloc 内存分配

#include <stdlib.h>
		malloc用来向系统申请size字节大小的内存(连续的),
			并且把这段内存的首地址返回(void*)
 void *malloc(size_t size);
			size : 你要分配多大的空间,单位是字节,你要多少字节的空间
      返回值 :
				成功 返回分配的空间的首地址  void*
          失败 返回NULL
//void* 表示这个东西是一个指针(地址),至于是什么类型的对象,他不关心.


#include <stdio.h>
#include <stdlib.h>

int main()
{
  int *p;

  //malloc 是一个函数,这个函数是系统写的,不是我们写的,一定要包含头文件
      //这个函数是 用来 从 堆空间中 分配内存.
  //p = (int*)malloc(sizeof(int));
  p = (int*)malloc(4);

  *p = 360;//p指向一个 堆空间对象
          //*p 就是表示p 指向的对象

  printf("%d\n",*p);


  return  0;
}

练习 :

用malloc实现动态数组.

int n;
scnaf("%d",&n);
int a[n]; //不可以的

int *a = malloc();

for(int i = 0;i < n;i++)
{
scanf("%d",...)
}

for(int i = 0;i < n;i++)
{
printf("%d ",*());
printf("%d ",a[i]);
}

#include <stdio.h>
#include <stdlib.h>


int main()
{
 //int n;
 //scnaf("%d",&n);
 //int a[n]; //不可以的
 
 int n;
 scanf("%d",&n);
 
 int *a = (int*)malloc(sizeof(int)*n);
 
 for(int i = 0;i < n;i++)
 {
     scanf("%d",a + i);
 }
 
 for(int i = 0;i < n;i++)
 {
     //printf("%d ",*(a + i));
     printf("%d ",a[i]);
 }
 printf("\n");
 
 
 return 0;
}

16.2 calloc 分配内存时同时清 0

#include <stdlib.h>

 		calloc 也是用来从"堆空间" 分配内存的,只不过calloc分配的内存会自动清0
    void *calloc(size_t n, size_t size);
			n : 要分配多少个元素
         size : 每个元素占多少个字节
             calloc 分配的总字节大小为 : n*size
         返回值 :
				成功 返回分配到的空间的首地址
             失败 返回NULL


#include <stdio.h>
#include <stdlib.h>


int main()
{
 //int n;
 //scnaf("%d",&n);
 //int a[n]; //不可以的
 
 int n;
 scanf("%d",&n);
 
 int *a = (int*)calloc(n,sizeof(int));
 
 for(int i = 0;i < n;i++)
 {
     //printf("%d ",*(a + i));
     printf("%d ",a[i]);
 }
 printf("\n");
 
 
 return 0;
}

16.3 realloc 再分配

#include <stdlib.h>

   		realloc 用来把ptr(malloc/calloc/realloc返回的地址,或者是NULL)
             指向的空间,扩大到size大小
    void *realloc(void *ptr, size_t size);
			1.
             ptr == NULL,首次分配
             	realloc(NULL,size)  <==> malloc(size)
             ptr != NULL
             	<1> size > 原来的大小
             		"扩建"
             	realloc用来把ptr指向的内存,扩大到size大小
             		原来的内存内容保持不变,后面新增的内存不会初始化.
             		他只负责分配空间.
             			(1)原址扩建
             			(2)整体搬迁
          2.
             size == 0
             	realloc(ptr,0) <==> free(ptr)
          	size < 原来的大小
             	这种情况,作者没有考虑到这种行为,这种行为是未定义的(什么结果都有可能发生)


16.4 free 释放

#include <stdlib.h>

   		free用来释放动态分配的空间的.
    void free(void *ptr);
			ptr : 要释放的空间的首地址,这个地址要是 malloc/calloc/realloc的返回值,其他地址
         返回值 :
				无

思考 :

  1. C语言中"内存泄漏/垃圾内存"

什么是内存泄漏?

内存泄漏有什么危害?

内存泄漏是如何产生的?

如何避免内存泄漏?

17 字符串 和 字符串函数

17.1 字符串

字符串 : 一串字符, "0个或多个字符"

单个字符, C语言中有专门的类型

​ char/unsigned char --> 保存的是单个字符的ASCII码(8bits的整数)

C语言中,没有字符串的类型.

C语言中的 字符串 是通过char*(字符型指针) 来实现的.

​ C语言的字符串是 一串字符(0个或多个) , 这些字符在内存地址上是连续的.

如果我们知道"字符串" 的首地址(第一个字符的地址) : 0x3000

推出字符串的第二个字符的地址 : 0x3001

推出字符串的第三个字符的地址 : 0x3002

...

并且我们知道每个字符的地址,就可以通过他的地址(char*)来访问每个字符.

通过字符串的首地址,不停的 +1,就可以得到每个字符的地址,

但是你通过字符的首地址,

能不能知道这个字符串有多少个字符呢?

​ 不能知道

所以,C语言规定字符串的末尾加一个 "字符串结束的标志"

null字符,空字符,0,'\0'

char* 用来保存字符串的首地址的,并且约定好字符串结束的标志'\0'.

你就可以通过char* 来访问或操作字符串.

char s[10];
	//数组名 s,当指针, &s[0], char*
scanf("%s",s);//%s从键盘上输入一串字符,输入到一个char*的地址中去
scanf("%s",&s[0]);//一样的

scanf("%s",&s);//不行的  类型不匹配%s --> char*
				//typeof(&s) ==> typeof(s)* ==> char[10]*

printf("%s\n",s);//%s输出一个字符串,后面也需要接一个字符串类型(char*)

17.2 字符串常量

字符串常量四不可以被修改的字符串,只能读.

字符串常量保存在一个.rodata的内存区.

如 :

C代码中,所有 " " 引起来的都是字符串常量
 并且 所有 用 " "引起的表达式的值, 就是这个字符串的首地址.
 typeof("abc123") : const char * //const 常量修饰词
 char* p = "acb123";//这是可以的
					//表达式 "abc123"的值是字符'a'的地址  &'a'
		p + 1 ==> c
     *(p + 1 ) = 'A';//不可以的 ""引起来的是常量,常量不能被改变 "aAb123"
		---------
       常量	 

内存

ro read only 只读区域

​ 存储哪些只能读的数据,常量 ,如 : "abcd"

rw read&write 可读可写区域

​ 存储那些变量

​ 如 : 没有加const 修饰的变量 ,数组,指针变量

int a;//a存储在可读可写区域
int b[100];//b存储在可读可写区域
char c[100];//c存储在可读可写区域
int *p;//p存储在可读可写区域
char ch;//ch存储在可读可写区域

17.3 字符串变量

把字符串(多个字符) 存储在一个可读可写的区域就可以了.

char s[100];//s存储在可读可写区域

你把字符串存储在s这个数组中,字符串变量.

char *p = (char*)malloc(100);//malloc 分配的空间也是 可读可写的
	你把字符串存储到p指向的空间,  字符串变量

如 :

char s[3];
s[0] = 'a';
s[1] = 'b';
s[2] = '\0';

===>
 char s[3] = {"ab"};
	s[0] = 'a';
 s[1] = 'b';
 s[2] = '\0';

	s[1] = 'B';//可以的  char s[3] = {"aB"}

练习 1 :

1.分析如下情况

char s1[] = {'a','b','c'};
char s2[] = {"abc"};
请问数组
 s1的元素个数是几个? 3  
 s2的元素个数是几个? 4
sizeof(s1) = ? 3
sizeof(s2) = ? 4
 
字符串s1中字符个数多少?  3
字符串s2中字符个数多少?  3
 
printf("%s\n",s1); //abc????
printf("%s\n",s2); //abc  

2.分析如下情况

char s3[5] = {'a','b','c'};
char s4[5] = {"abcd"};

sizeof(s3) = ? 5
sizeof(s4) = ? 5
 
printf("%s\n",s3); abc
printf("%s\n",s4); abcd

3.分析如下程序的输出结果

char s[10] = {"abcde"};
s += 2;//error s = s + 2 //s是一个数组名 常量

s[2] = 'A';//可以的  数组s是可读可写
*(s + 2) = 'A';//*(&s[0] + 2) ==> s[2]

printf("%s\n",s);//abAde  abAdA  

4.分析如下程序的输出结果

char *s = "abcde";
s += 2;//可以的  s是一个指针变量

*(s + 2) = 'A';//不可以的  s指针的空间 只读空间
printf("%s\n",s);

#include <stdio.h>

int main()
{
 //printf("%s\n","abcd123");//可以  "abc123" 类型是char *,值为 首字符的地址.
 char *p = "abcd123";
 //printf("%s\n",p);//可以的
 //printf("%s\n",p + 2);

 *(p + 2) = 'A';//error
 printf("%c\n",*(p + 2));


 return 0;
}

17.4 几个常用的字符串处理函数

(1) strlen 用来求一个字符串的长度

NAME
    strlen - calculate the length of a string

SYNOPSIS
    #include <string.h>
		    strlen 求一个字符串的长度
    size_t strlen(const char *s);
			@s : s指向的字符串,遇到'\0'结束,求的长度不包含'\0'
         返回值 :
				返回字符串的长度

例子 :

int l;
l = strlen("abcde");
l = 5
 
char s[10] = {"abc"};
l = strlen(s);
l = 3
 
strlen : 是一个函数,用来计算一个字符串的长度的,算到第一个 '\0' 为止.
sizeof : 是一个运算符,用来求一个对象(或类型)自己本身所占内存的字节数
 
char s1[4] = {'1','0'};
sizeof(s1) = ? 4 
l = strlen(s1);
l = ? 2
 
char s2[4] = {'1',0,'3'};
sizeof(s2) = ? 4
l = strlen(s2);
l = ? 1
 
l = strlen("abcd\123456\0ef");
l = ?8   //\123算一个字符

(2) strcpy 字符串拷贝函数

cpy : copy

NAME
    strcpy, strncpy - copy a string

SYNOPSIS
    #include <string.h>
			strcpy用来把src指向的字符串拷贝到dest指向的空间中去,
				拷贝到 \0 为止(\0也会拷贝)
    char *strcpy(char *dest, const char *src);
				dest : 指向 "目的地",dest指向的空间,要保证可写,空间要足够大
             src : 指向"源字符串",把src指向的字符串拷贝到dest指向的空间中
             返回值 :
					成功  返回拷贝后字符串的首地址.


		strcpy有一个巨大的bug :
			他没有考虑到越界的问题,  有可能会导致内存的非法访问
             
			strncpy 就是为了解决strcpy的bug而产生的.
         strncpy用来把src指向的字符串拷贝到dest指向的空间中去,
				但是strncpy顶多拷贝n个字符(一般来说,n为dest指向空间的最大容量)
                 那么strncpy到底拷贝了多少个字符呢?  <=  n
                 (1) 遇到 \0 拷贝结束(此时后面顶多拷贝 n - strlen(src)个 \0)
                 (2) 一直没有遇到\0,但是已经拷贝了n 个字符,拷贝结束(此时\0不会拷贝)
    char *strncpy(char *dest, const char *src, size_t n);
				dest : 指向目标空间
             src : 指向源字符串
             n : 规定拷贝 n 个字节,一般来说,n为dest指向空间的最大容量
             返回值 :
					成功 返回拷贝后字符串的首地址(dest)

#include <stdio.h>
#include <string.h>

int main()
{
 char s[10];

 strcpy(s,"abcde");

 printf("%s\n",s);

 return 0;
}

#include <stdio.h>
#include <string.h>

int main()
{
 // char s[4];
 // strcpy(s,"abcde"); //有问题的

 char s[4];
 strncpy(s,"abcde",3);
 printf("%s\n",s);

 return 0;
}

练习 :

​ 写一个函数, 实现strncpy的功能.

char* my_stncpy(char* dest,char* src,size_t n)
{
 for(int i = 0;i < n;i++)
 {
     *dest = *src;
     dest++;
     src++;
 }
 *dest = '\0';
 
 return dest;
}


char* strncpy(char *dest, const char *src, size_t n)
{
 size_t i;

 for (i = 0; i < n && src[i] != '\0'; i++)
 {
     dest[i] = src[i];
 }
     
 for ( ; i < n; i++)
 {
     dest[i] = '\0';
 }
     
 return dest;
}



#include <stdio.h>
#include <string.h>

int main()
{
 char s1[8];
 char s2[8] = {"abcde"};
 printf("%p\n%p\n",s1,s2);

 //strcpy(s1,"123456789");
 //strncpy(s1,"123456789",8);
 strncpy(s1,"ABCDEF\0ab",9);

 printf("%s\n",s1);
 printf("%s\n",s2);
 return 0;
}


(3) mencpy 内存拷贝

NAME
    memcpy - copy memory area

SYNOPSIS
    #include <string.h>
			memcpy 用来把src指向的内存中的前面n个字节,拷贝到dest指向的空间中去.
    void *memcpy(void *dest, const void *src, size_t n);
				dest : 指向目标内存块,要保证dest指向的空间足够大
             src : 指向源内存块
             n : 要拷贝的字节数
             返回值 :
					返回  dest的这个地址

思考 :

​ strncpy 和 memcpy 有什么区别?

strncpy是字符串的拷贝函数,顶多拷贝n个字节(遇到\0拷贝结束,多补几个 n - i个\0)

memcpy是内存块拷贝函数(没有类型概念) ,成功的话, 一个会拷贝n个字节.

so ,

​ 字符串拷贝,最好用 strncpy

​ 其他内存拷贝,用memcpy

例子 :

char s1[8];
char s2[8];
scanf("%s",s2);
	//从键盘上输入一个字符串(遇到\n),存放到s2指向的空间中去
	//最后加一个\0到s2这个空间

//用户输入的字符串长度不定
//要把s2指向的字符串, 拷贝到s1
memcpy(s1,s2,strlen(s2) + 1);
 //"+1" 我想把s2这个字符串后面\0也拷贝
 //strlen(s2) + 1 <= 8 没有问题
 //strlen(s2) + 1 > 8 就有问题了,越界了
 
strncpy(s1,s2,8);
	//strlen(s2) < 8 可以完整的拷贝字符串到s1
	//strlen(s2) >= 9 也不会越界,因为顶多拷贝8个

(4) bzero 把一个字符串清0

AME
    bzero - zero a byte string

SYNOPSIS
    #include <strings.h>
			bzero用来把s指向的空间的前面n个字节清0
    void bzero(void *s, size_t n);


(5) strcat/strncat 连接两个字符串

NAME
    strcat, strncat - concatenate two strings

SYNOPSIS
    #include <string.h>
			strcat 用来把src指向的字符串,连接到dest指向的字符串的末尾去."尾部连接"
 			strcat先找到dest指向的那个字符串的末尾(\0),从\0处开始接src指向
 				的字符串,最后加一个\0
    char *strcat(char *dest, const char *src);
				dest : 指向目标字符串,其空间要足够大
                 	why? 如果不足够大,就有越界的风险.
				src : 指向源字符串
             返回值:
					返回 连接后的字符串的首地址(dest)
                     
          strcat也有一个bug,没有考虑到dest指向的空间,是否足够大的问题,有越界的风险
                     
          strncat 就是用来修复strcat这个bug的
           strcat 用来把src指向的字符串,连接到dest指向的字符串的末尾去.
                但是他顶多拷贝n个字符
             (1) 遇到\0拷贝结束  <= n
             (2) 即使没有遇到\0,但是已经拷贝了n个字符啦,也结束(此时\0不会拷贝)
    char *strncat(char *dest, const char *src, size_t n);


A simple implementation of strncat() might be:

        char *
        strncat(char *dest, const char *src, size_t n)
        {
            size_t dest_len = strlen(dest);
            size_t i;

            for (i = 0 ; i < n && src[i] != '\0' ; i++)
                dest[dest_len + i] = src[i];
            dest[dest_len + i] = '\0';

            return dest;
        }


(6) strcmp/strncmp 比较两个字符串

str : string 字符串

cmp : compare 比较

strcmp 是字符串比较函数

两个字符串如何比较呢?

一个一个字符PK,PK他们的ASCII码

while(第一个字符没有结束 || 第二个字符串没有结束)
{
 if  第一个字符串的第i个字符  < 第二个字符串的第i个字符
     return -1;//表示第一个字符串 小
 else if 第一个字符串的第i个字符 > 第二个字符串的第i个字符
     return 1;//表示第一个字符串 大
 else  //第一个字符串的第i个字符 == 第二个字符串的第i个字符
     i++;//比较下一个
}

return 0;//表示两个字符串相等,一模一样

NAME
    strcmp, strncmp - compare two strings

SYNOPSIS
    #include <string.h>

    int strcmp(const char *s1, const char *s2);
			
			strncmp 只比较前面的n个字符
    int strncmp(const char *s1, const char *s2, size_t n);


例子 :

int m = strcmp("123","ABC")
 m < 0

m = strcmp("123","123\0abc")
 m == 0
 
m = strcmp("1234","123")
 m > 0
 
m = strncmp("1234","123",3);
	m == 0

posted @ 2023-07-22 15:29  风恬月淡时  阅读(121)  评论(0)    收藏  举报