Linux--C语言之指针

news2024/9/23 9:28:49

文章目录

  • 一、指针的引入
  • 二、指针概述
  • 三、指针变量
    • (一)指针变量的定义
      • 语法:
      • 举例:
      • 注意:
    • (二)指针变量的使用
      • 1. 指针变量的赋值
      • 2. 操作指针变量的值
      • 3. 操作指针变量指向的值
      • 4. 两个有关运算符的使用:
      • 案例:
    • (三)指针变量做函数参数
      • 案例:
  • 四、数组指针与指针数组
    • (一)通过指针引用数组
      • 1. 数组元素的指针
      • 2. 指针的运算
      • 3. 数组名做函数参数
      • 4. 实参为指针变量,形参为数组名。
      • 5. 数组指针
    • (二)函数的传参
      • 1. 值传递:
      • 2. 引用传递:
    • (三)数组指针
      • 1. 概念:
      • 2. 特点:
      • 3. 一维数组指针
      • 4. 二维数组指针
    • (四)指针数组
      • 1. 概念:
      • 2. 特点:
      • 3. 语法:
  • 五、字符串指针
    • (一)字符串实现
    • (二)字符数组和字符指针的联系
    • (三)字符指针作函数参数
  • 六、函数指针与指针函数
    • (一)函数指针
      • 1. 定义:
      • 2. 函数指针存在的意义:
      • 3. 定义格式:
      • 4. 函数指针的初始化
      • 总结:
    • (二)指针函数
      • 1. 定义:
      • 2. 定义格式:
      • 注意:
  • 七、野指针与空指针
    • (一)野指针
      • 1. 定义:
      • 2. 野指针产生的场景:
      • 3. 如何避免野指针:
    • (二)空指针
  • 八、二级指针
    • 1. 定义:
    • 2. 定义格式:
    • 举例:
    • 结论
    • 3. 二级指针的用法:
  • 九、main函数的函数原型
    • 1. 定义:
    • 2. main函数的完整写法:
    • 3. 扩展写法:
    • 说明:
    • 注意:
  • 十、常量指针与指针常量
    • 1. 常量:
    • 2. 语法:
    • 3. 常量指针
      • 定义:
      • 定义格式:
      • 结论
      • 应用场景:作为形式参数,实际参数需要给一个常量。
      • 案例:
    • 4. 指针常量
      • 常量指针常量

一、指针的引入

  1. 为函数修改实参提供支持。
  2. 为动态内存管理提供支持。
  3. 为动态数据结构提供支持。
  4. 为内存访问提供另一种途径。

二、指针概述

  1. 内存地址:系统为了内存管理的方便,将内存划分为一个个的内存单元(1个内存单元占1个字节),并为每一个内存单元进行了编号,内存单元的编号称为该内存单元的地址。一般情况下,我们每一个变量都是由多个内存单元构成的,所以每个变量的内存地址,其实就是这个变量对应的第一个内存单元的地址,也叫首地址
  2. 变量指针:变量地址称为该变量的指针。变量地址往往是指变量在内存中的第一个内存单元的编号(首地址)。
  3. 指针变量:存放其他变量地址的变量
  4. 指向:指针变量中存放“谁”的地址,就说明该指针变量指向了“谁”。
  5. *:指针运算符

案例:

// 指针初识

#include <stdio.h>

void main()
{
	// 定义一个普通/一般变量
	int i = 3;
	// 定义一个指针变量,并赋值
	int *i_point = &i; // 指针变量的数据类型要和存储的地址变量类型一致
	// 访问普通变量(直接访问) 
	printf("直接访问-%d\n",i);  // 3
	
	// 访问指针(地址访问)%p访问地址
	printf("地址访问-%p\n",i_point);

	// 访问指针变量(间接访问)
	printf("间接访问-%d\n",*i_point);  // 3
}

三、指针变量

(一)指针变量的定义

语法:

数据类型 *变量列表;

举例:

int a; // 普通变量
int *a,*b; // 指针变量

注意:

  1. 虽然定义指针变量*a,是在变量名前加上*,但实际变量名为a,而不是*a

  2. 使用指针变量间接访问内存数据时,指针变量必须要有明确的指向;

  3. 如果想借助指针变量间接访问指针变量保存的内存地址上的数据,可以使用指针变量前加*来间接访问;

  4. 指针变量前加*,也称为对指针变量解引用

    int i = 5, *p;
    p = &i; // 将i的地址赋值给指针变量p
    
    printf("%d\n",*p);// 间接访问i的值,也称为解引用p对应地址空间的值
    
  5. 指针变量只能指向同类型的变量,借助指针变量访问内存,一次访问的内存大小是取决于指针变量的类型;

  6. 指针变量在定义同时可以初始化:这一点和普通变量是一样的。

    int i = 5;
    int *p = &i; // 定义的同时初始化
    

(二)指针变量的使用

1. 指针变量的赋值

// 方式1
int a,*p;
p = &a; // 指针变量的值是其他变量的地址

// 方式2
int a,*p,*q = &a;
p = q;

2. 操作指针变量的值

int a,*p,*q = &a;
p = q;

printf("%p",p); // 此时返回的是变量a的地址空间

3. 操作指针变量指向的值

int a = 6,*q = &a;

printf("%d",*q); // 6

4. 两个有关运算符的使用:

  • : 取地址运算符。 &a是变量a的地址。
  • *:指针运算符 (或称“间接访问”运算符),*p是指针变量p指向的对象的值。

案例:

#include <stdio.h>
void main()
{
	int a,b;
    int *pointer_1, *pointer_2;
	a=100; b=10;
	pointer_1=&a;
	pointer_2=&b;
    
	printf("a=%d,b=%d\n",a,b);
	printf("pointer_1=%d,pointer_2=%d\n",*pointer_1,*pointer_2);
}

案例1:声明a,b两个一般变量,使用间接存取的方式实现数据的交换?

在这里插入图片描述

代码:

/**
* 需求:声明a,b两个一般变量,使用间接存取的方式实现数据的交换?
*/
#include <stdio.h>
void main()
{
	// 声明5个变量
	int a = 3,b = 5,*p_a=&a,*p_b=&b,*p_t;
    
	// 交换前输出
	printf("%d,%d\n",*p_a,*p_b);
    
	// 交换位置
	p_t = p_a;
	p_a = p_b;
	p_b = p_t;
    
	// 交换后输出
	printf("%d,%d\n",*p_a,*p_b);
}

案例2:指针变量应用。输入a、b两个整数,按先大后小的顺序输出a和b。

在这里插入图片描述

代码:

/*
需求:指针变量应用。输入a、b两个整数,按先大后小的顺序输出a和b。
*/
#include <stdio.h>
void main()
{
	int a = 3,b = 5,*p_a = &a,*p_b = &b,*p_t;
    
	if( a < b)
	{
		p_t = p_a;// 操作指针变量,不会影响到数据本身
		p_a = p_b;
		p_b = p_t;
	}
    
	printf("按从大到小输出a,b的值:%d > %d\n",*p_a,*p_b);
}

代码:不推荐

/*
需求:指针变量应用。输入a、b两个整数,按先大后小的顺序输出a和b。
*/
#include <stdio.h>
void main()
{
	int a = 3,b = 5,*p_a = &a,*p_b = &b,*p_t;
    
	if( a < b)
	{
		*p_t = *p_a;// 操作指针地址指向的内存空间,也就是直接操作变量a
		*p_a = *p_b;
		*p_b = *p_t;
	}
    
	printf("按从大到小输出a,b的值:%d > %d\n",*p_a,*p_b);
}

(三)指针变量做函数参数

指针变量做函数参数往往传递的是变量的地址(首地址),借助于指针变量间接访问是可以修改实参变量数据的。

案例:

需求:需要用函数处理,用指针变量做函数的参数

  1. 方式1:交换指向(指向的普通变量的值不变)
// 需求:指针变量应用。输入a、b两个整数,按先大后小的顺序输出a和b。
#include <stdio.h>

// 自定义函数,实现两个数的比较
void swap(int *p_a,int *p_b)
{
    int *p_t;
    // 这种写法,只会改变指向,不会改变地址对应空间的数据
    p_t = p_a;
    p_a = p_b;
    p_b = P_t;
    
    printf("%d > %d\n",*p_a,*p_b); // 5 > 3
}

// 主函数
void main()
{
    int a = 3,b = 5;
    
    if(a < b)
    {
        swap(&a,&b);  // int *p_a = &a,int *p_b = &b
    }
    printf("%d > %d\n",a,b);// 3 > 5
}
  1. 方式2:交换值(指向的普通变量的值改变)
// 需求:指针变量应用。输入a、b两个整数,按先大后小的顺序输出a和b。
#include <stdio.h>

// 自定义函数,实现两个数的比较
void swap(int *p_a,int *p_b)
{
    int t;
    // 这种写法,不会改变指向,改变的是地址对应空间的数据
    t = *p_a;
    *p_a = *p_b;
    *p_b = t;
    
    printf("%d > %d\n",*p_a,*p_b); // 5 > 3
}

// 主函数
void main()
{
    int a = 3,b = 5;
    
    if(a < b)
    {
        swap(&a,&b);  // int *p_a = &a,int *p_b = &b
    }
    printf("%d > %d\n",a,b);// 5 > 3
}

四、数组指针与指针数组

(一)通过指针引用数组

1. 数组元素的指针

  • 数组指针:数组中的第一个元素的地址,也就是数组的首地址。
  • 指针数组:用来存放数组元素地址的数组,称之为指针数组。
  • 在C语言中,由于数组名代表数组的首地址,因此,数组名实际上也是指针。
// 定义一个一般数组
int a[] = {1,4,9};

// 使用指针变量存储数组的第一个元素的首地址,也就是数组的首地址
int *p = &a[0]; 

int *p = a; // 意味着:int *p = &a[0] 完全等价于 int *p = a;

print{"%d\n",*p}; // 1

注意:虽然我们定义了一个指针变量接收了数组的地址,但不能理解为指针变量指向了数组,而应该理解为指向了数组的元素。

2. 指针的运算

  • 指针运算:指针变量必须要指向数组的某个元素。
序号指针运算说明
1自增:p++、++p、p = p + 1\p += 1让指针变量指向下一个元素
2自减:p–、–p、p = p - 1\p -= 1让指针变量指向上一个元素
3加一个数:p + 1下一个元素的(首)地址
4减一个数:p - 1上一个元素的(首)地址
5指针相减:p1 - p2p1,p2之间相差几个元素(注:只有p1和p2都指向同一数组中的元素时才有意义)
6指针比较:p1 < p2前面的指针小于后面的指针

案例1:

#include <stdio.h>

int main()
{
	// 定义一个一般数组
	int a[] = {1,3,5,7,9};
	
	// 计算数组中元素的个数
	// sizeof用法:sizeof(运算数) 或者 sizeof 运算数
	int length = sizeof a / sizeof a[0];

	// 创建指针变量
	int *p = a;

	// 定义循环变量,register关键字:用于指示编译器,将变量存储在处理器中的寄存器中,从而提高程序的运行效率
	register int i;

	// 遍历
	for(i = 0; i < length ; i++)
	{
		printf("[1] %d ",a[i]);  // 下标法
		printf("[2] %d ",*(a+i)); // 指针法,但是这种写法,需要注意,a+i无法修改数组,只读
		printf("[3] %d \n",*(p+i)); // 指针法,这种更为灵活,可读可写,建议这种写法


			
		// printf("%d ",*p); 等价上面的写法
	    // p++;
	}
	
	printf("\n");	

	return 0;
}

案例2:

/**
 *	① p++;*p;  先使p指向下一元素,然后再取其值;
    ② *p++ 		相当于 *(p++),即先求*p,再作p++
    ③ *(++p)    先使p++ ,再取*p
    ④ (*p)++    使所指向的元素值加1,而不是指针值加1
 */ 


#include <stdio.h>

int main()
{
	// 定义一个测试数组
	int a[] = {10,22,33,44,55,66,77,88};

	// 定义指针变量并初始化
	int *p = a;
	
	p++; // 指针+1,元素值不改变 元素:22
	printf("%d\n",*p); // p=p+1=1; 输出元素:22

	int x = *p++; // x = 22,p++ --> *p:33
	printf("%d,%d\n",x,*p); // *p++:先*p,再p++,元素输出:22,33
	
//	printf("%d,%d\n",*p++,*p); // 输出元素:22,22


	int y = *(++p); // ++p,y=44;
	printf("%d,%d\n",y,*p); // *(++p):先p++,再*p,元素输出:44,44
//	printf("%d,%d\n",*(++p),*p); // 输出元素:44,33


	(*p)++; // 元素值+1,指针不改变
	printf("%d\n",*p); // index为的元素值:44+1=45

	return 0;
}

3. 数组名做函数参数

  • 表现形式:

    • 形参和实参都用数组名;

    • 实参用数组名,形参用指针变量;

    • 实参形参都用指针变量;

      void fun(int *p1)
      {
          
      }
      
      void main()
      {
          // 如果实参和形参都是指针,我们需要给实参进行初始化操作
          int arr[2] = {23,34};
          int *p0 = arr; // 初始化
          
          fun(p0);
      }
      
      

4. 实参为指针变量,形参为数组名。

案例:

/**
* 需求:将数组a中n个整数按相反顺序存放。
*/

#include <stdio.h>

/* 数组的反转:方式1——数组实现*/
void inv1(int arr[],int len)
{
	// 反转思路:将第0个和n-1个进行对掉,将第1个和n-2个对掉..
	// 定义循环变量i,临时变量temp
	int i,temp;

	// 遍历数组
	for(i = 0; i < len / 2; i++)
	{
		temp = arr[i];
		arr[i] = arr[len - 1 - i];
		arr[len - 1 - i] = temp; 
	}
}


/* 数组的反转:方式2——指针实现*/
void inv2(int *arr,int len)
{
	// 反转思路:将第0个和n-1个进行对掉,将第1个和n-2个对掉..
	// 定义循环变量i,临时变量temp *j = &arr[len -1] 等价于 arr + len -1
	int *i = arr,*j = &arr[len-1],temp;

	// 遍历数组
	for(; i < j; i++,j--)
	{
		temp = *i;
		*i = *j;
		*j = temp; 
	}
}


// 主函数
int main()
{
	// 创建一个数组
	int array[10] = {12,23,55,88,66,99,32,54,11,33};
	
	// 计算数组大小
	int length = sizeof(array) / sizeof(int);

	inv2(array,length);

	// 测试是否反转
	for(int i = 0; i < length; i++)
	{
		printf("%d ",array[i]);
	}

	printf("\n");

	return 0;
}

5. 数组指针

  • 数组指针:指向一维数组的指针变量。
  • 数组指针的定义:假定该指针变量指向具有N个元素的一维数组,则数组指针变量定义如下:
数据类型 (*数组指针变量名)[N]

一维数组:

int a[N] = {1,2,3};
int (*p)[N] = &a;

二维数组:

int a[][N] = {1,3,5,7};
int (*p)[N] = a[0]; // 等价于 &a[0][0]

分析:

int arr[3] = {1,2,3};
int *p = arr; // &arr[0] 首地址:第1个元素的首地址
int (*p)[3] = &arr;
--------------------------------------------------
int arr[][3] = {1,2,3,4,5,6};
int *p = arr[0]; // &arr[0][0] 首地址:第1行第1列元素的首地址
int (*p)[3] = &arr[0];

案例:

/**
 * 数组指针:输出二维数组任一行任一列元素的值。
             此时:我们需要将二维数组看作是一个特殊的一维数组
 */
#include <stdio.h>
int main()
{
    int a[3][4] = {1,3,5,7,9,11,13,15,17,19,21,23};
    int (*p)[4]; // 我们把行使用指针表示
    
    p = &a[0]; // 等价于 p = a
    int i,j;// 代表我们要显示的数据对应的行号和列号
    printf("请输入行号和列号:\n");
   
    scanf("i=%d,j=%d",&i,&j);
    printf("a[%d][%d]=%d\n",i,j,*(*(p+i)+j));
    return 0;
}

(二)函数的传参

1. 值传递:

一般发生在函数形参的类型为char、short、int、long、float、double…这样的类型,它的传值,一般是实参将自己的值复制一份给形参,也就是实参变量和形参的变量空间是分开的,不共享的。此时形参无法改变实参的数据。

2. 引用传递:

一般发生在函数形参的类型为数组、指针…这样的类型,它的传递,一般是实参将自己的内存首地址复制一份给形参,也就是实参变量和形参变量对应的内存空间是同一个,共享的。此时形参可以改变实参的数据。

(三)数组指针

1. 概念:

数组指针是指向数组的指针。

2. 特点:

  • 先有“数组”,后有“指针”
  • 它指向的是一个完整的数组

3. 一维数组指针

  • 语法:

    数据类型 (*指针变量名)[容量];
    
  • 案例:

    #include <stdio.h>
    
    int main()
    {
        // 先有数组,再有指针
        int arr[3] = {100,200,300};
        
        // 获取数组的元素个数
        int len = sizeof arr / sizeof arr[0];
        
        // 定义一个数组指针,指向arr数组
        // 数组指针的语法:数据类型 (*指针变量)[容量]
        int (*p)[3] = &arr; // 此时p不是指向arr数组的第一个元素,而是指向arr这个数组本身;int *p = &arr[]或arr  这种写法,p指向的不是数组本身,而是指向的个元素
    	
    	printf("%p\n",p);
    	// p++; 此时p++会跳出整个数组
    	// printf("%p\n",p);
    	
    	printf("%d\n",(*p)[2]); // 300 
       
        // 遍历
        for(int i = 0; i < len; i++)
        {
            printf("%d ",(*p)[i]);
        }
        
        printf("\n");
    }
    

4. 二维数组指针

  • 语法:

    数据类型 (*指针变量名)[容量];
    
  • 案例:

    // 二维数组指针案例
    
    #include <stdio.h>
    
    int main()
    {
    	// 数组指针:先有数组,再有指针
    	// 创建一个二维数组
    	int arr[][3] = {{6,8,9},{66,88,99},{666,888,999}};
    
    	// 创建一个数组指针,指向二维数组
    	// int (*p)[3] = &arr; // p[0] -->{100,200,300},p[1] -->{1000,2000,3000},也就是说:p[0] = 元素100的首地址,p[1] = 元素1000的首地址
    	int (*p)[3] = arr; // arr等价于&arr[0],p[0] = 元素100的首地址
    	
    	// 获取元素6?
    	printf("6-%d\n",*p[0]); // arr[0][0]
    
    	// 获取元素88?
    	printf("88-%d %d %d",p[1][1],*(p[1]+1),*(*(p+1)+1)); // arr[i][j] <===> *(*(p+i)+j) <===> *(p[i]+j) <===> p[i][j]
        printf("\n");	
    
    	// 获取元素999?
    	printf("999-%d %d %d",p[2][2],*(p[2]+2),*(*(p+2)+2));
        printf("\n");
    	
    	return 0;
    }
    
  • 指针和数组中符号优先级:()>[]>*

(四)指针数组

1. 概念:

指针数组是一个数组,数组中的每个元素都是一个指针。

2. 特点:

  • 先有“指针”,后有“数组”;
  • 指针数组的本质是一个数组,只是数组中的元素类型为指针。

3. 语法:

数据类型 *数组名[容量];
  • 案例:

    // 指针数组案例
    
    #include <stdio.h>
    
    int main()
    {
    	// 定义三个变量
    	int a = 10, b = 20, c = 30;
    
    	// 定义指针数组,指针数组用来存放指针(变量或者常量的内存地址)
    	int *arr[3] = {&a, &b, &c};
    
    	// 获取数组大小
    	int length = sizeof arr / sizeof arr[0];
        
    	//遍历
    	for(int i =0; i < length; i++)
    	{
    		printf("%d ",*(arr[i])); // 输出每个指针所指向的值,需要解引用
    	}
    
    	printf("\n");	
    
    	return 0;
    }
    
  • 建议:我们一般使用指针数组处理字符串

五、字符串指针

(一)字符串实现

在C语言中,表示一个字符串有以下两种方式:

  1. 用字符数组存放一个字符串;
  2. 用字符指针指向一个字符串。

案例:

// 字符串的两种实现方式

#include <stdio.h>

/* 使用字符数组实现 */
void fun1()
{
	// 定义伪字符串
	char str1[] = "I Love China!";
	
	printf("%s\n",str1);
}

void fun2()
{
	// 定义伪字符串
	char *str2 = "I Love China!";
	
	printf("%s\n",str2);
}

/* 使用字符指针实现 */

int main()
{
	fun1();
	fun2();

	return 0;
}

注意:字符数组和字符指针变量都能实现字符串的存储与运算。

(二)字符数组和字符指针的联系

  1. 字符数组由元素组成,每个元素中存放一个字符,而字符指针变量存放的是地址,也能作为函数参数。
  2. 只能对字符数组中的各个元素赋值,而不能用赋值语句对整个字符数组赋值。
  3. 字符数组名虽然代表地址,但数组名的值不能改变。因为数组名是常量。
  4. 对于字符串中字符的存取,可以用下标法,也可以用指针法。

案例:

// 字符数组和字符指针的联系

#include <stdio.h>

int main()
{
	// 使用两种方式定义字符串
	char str1[] = "I Love China!";
	char *str2 = "Hello World!";

	// 测试赋值
	// str1 = "Hello world!"; // 不能对字符数组整体赋值,如果要赋值,请使用string.h中strcpy()
	str2 = "I Love China!";

	// 打印输出
	printf("%s\n%s\n",str1,str2);

	// 使用下标法和指针法访问字符串
	printf("%c\n%c\n",str1[4],*(str2+4)); // v v 空格也是占位的
		
	return 0;
}

(三)字符指针作函数参数

  1. 实参与形参都用字符数组名

  2. 实参用字符数组名,形参用字符指针变量(在函数内部不能对字符串中的字符做修改)

fun(char *arr,int len)
{
    arr[2] = 'A'; // 错误,字符串常量一旦创建,就不能被改变
}
int main()
{
    char arr[] = "abc"; // 字符串常量,常量是不可修改的
    fun(arr,3);
}
  1. 实参与形参都用字符指针变量(在函数内部不能对字符串中的字符做修改)
fun (char *arr,int len)
{
     arr[2] = 'A'; // 错误
}
int main()
{
    char arr[] = "abc"; 
    char *p = arr; // &arr[0]
    
    fun(p,3);
}

  1. 实参用字符指针变量,形参用字符数组名

注意:

  1. 字符数组在创建的时候,会在内存中开辟内存空间,内存空间可以存放字符数据;字符指针在创建的时候,需要依赖于字符数组,字符指针在内存中开辟的内存空间中,存放的是数组元素的内存地址。字符指针的创建依赖于字符数组,字符数组可以独立存在,而字符指针不能独立存在

  2. 字符数组可以初始化,但是不能赋值;字符指针可以初始化,也可以赋值。

    // 字符数组
    char str1[11] = "I Love China!"; // 正确
    str1 = "home"; // 错误
    str1[] = "hmoe"; // 错误
    
    // 字符指针
    char *str2 = "I Love China!"; // 正确
    str2 = "home"; // 正确
    

案例1:

/**
* 字符指针作为函数参数:用函数调用实现字符串复制以及计算字符串长度。
*/

#include <stdio.h>

/* 定义一个函数,传递拷贝的字符串,返回字符串长度 */
int str_copy(char *str1,char *str2)
{
	// 定义循环变量
	int i = 0;

	while(str1[i] != '\0')
	{
		// 实现拷贝
		str2[i] = str1[i];
		i++;
	}
	
	// 拷贝结束之后,一定要给字符串加上结束符号\0
	str2[i] = '\0';

	// 返回字符串长度
	return i;
}

int main()
{
	// 定义两个数组,从键盘录入字符串
	char str1[20],str2[20];

	// 输出提示信息
	printf("请输入字符串:\n");
	scanf("%s",str1);

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

	int len = str_copy(str1,str2);

	printf("str2=%s\n",str2);
	printf("len=%d\n",len);

	return 0;
}

案例2:

/**
* 给定一个字符串,截取start到end之间字符串,含头不含尾
*/

#include <stdio.h>

/* 字符串截取函数 */
int str_split(char *str,int start,int end,char *temp)
{
	// 定义变量
	int i=0,k=0;
	
	while(str[i] != '\0')
	{
		if(i >= start && i < end)
		{
			temp[k] = str[i];
			k++;
		}
		
		i++;
	}

	temp[k] = '\0';

	// 更新str中的数据
	printf("%s\n",temp);	

	printf("\n");

	return k;
}

int main()
{
	char *str = "abcdefg";
	char temp[200];
	
	int len = str_split(str,2,5,temp);

	printf("截取的字符串是:%s,长度为:%d\n",temp,len);

	return 0;
}

六、函数指针与指针函数

(一)函数指针

1. 定义:

函数指针本质是指针,它是函数的指针(定义一个指针变量,变量中存储了函数的地址)。函数都有一个入口地址,所谓指向函数的指针,就是指向函数的入口地址。这里函数名就代表入口地址。

2. 函数指针存在的意义:

  • 让函数多了一种调用方式
  • 函数指针作为形参,可以形式调用(回调函数)

3. 定义格式:

返回值类型(*变量名) (形式参数列表);

举例:

int (*p) (int a,int b);

4. 函数指针的初始化

  • 定义同时赋值

    // 要先有函数,才能定义这个函数指针
    int fun(int a,int b){..}
    
    // 定义函数指针并给它赋值
    int (*p) (int a,int b) = fun; // fun不能跟()
    
  • 先定义后赋值

    // 要先有函数,才能定义这个函数指针
    float fun(int a,double b,char c){..}
    
    // 先定义函数指针
    float (*p) (int a,double b;char c);
    // 赋值
    p = fun;
    

总结:

  1. 函数指针指向的函数要和函数指针定义的返回值类型、形参列表对应,否则编译报错。
  2. 函数指针是指针,但不能指针运算,如:p++等,没有实际意义。
  3. 函数指针作为形参,可以形成回调(回调后面讲解)。
  4. 函数指针作为形参,函数调用时的实参只能是与之对应的函数名,不能带小括号。
  5. 函数指针的形参列表中的变量名可以省略。

案例:

/**
*  函数指针:指向函数的指针变量就是函数指针
   需求:求a,b两个数的最大值
*/

#include <stdio.h>

// 自定义求两个数最大值函数
int max(int a,int b)
{
	if(a > b)
	{
		return a;
	}
	
	return b;
}

int main()
{
	// 声明变量
	int a = 2,b = 3,c;
	
	// 普通函数调用
	c = max(a,b);
	printf("%d,%d两个数中的最大值是:%d\n",a,b,c);

	// 通过指针变量访问它指向的函数
	// 创建指针并初始化
	int (*p)(int ,int ) = max;
	
	// 调用函数指针
	c = p(a,b);
	printf("%d,%d两个数中的最大值是:%d\n",a,b,c);

	// 调用函数指针
	c = (*p)(a,b);
	printf("%d,%d两个数中的最大值是:%d\n",a,b,c);

	return 0;
}

(二)指针函数

1. 定义:

本质是函数,这个函数的返回值类型是指针,这个函数称为指针函数。

2. 定义格式:

指针类型 函数名(形成列表)
{
    函数体;
    return 指针变量;
}

举例:

int *get(int a)
{
    int *b = &a;
    // return &a; // 编译报警告
    
    return b;
}

注意:

在函数中不要直接返回一个局部变量的地址,因为函数调用完毕后,局部变量会被回收,使得返回的地址就不明确,此时返回的指针就是野指针。

解决方案:

​如果非要访问,可以给这个局部变量添加static,可以延长它的生命周期,从而避免野指针(尽量少用,因为存在内存泄漏)。

案例:

/**
*	指针函数:函数的返回值是指针类型
	需求:有若干个学生的成绩(每个学生有4门课程),要求在用户输入学生序号以后,能输出该学生的全部成绩。用指针函数来实现。
*/

#include <stdio.h>

/* 定义一个函数,传入学生的序号,返回这个学生的所有课程成绩 */
float *search(float (*p)[4],int n)
{
	// 定义一个指针,用来接收查询到的某个学生的所有课程成绩
	float *pt;
	
    pt = *(p+n);
	
	return pt;
}

int main()
{
	// 创建一个二维数组存放学生的课程成绩
	float score[][4] = {{66,77,88,99},{60,70,80,90},{55,68,76,98}};

	// 声明变量
	int i,n;
	float *p;
	
	printf("请输入学生序号(0-2):\n");
	scanf("%d",&n);

	printf("第%d个学生的全部成绩如下:\n",n);
	
	p = search(score,n); // 函数返回值为行的首地址

	// 遍历
	for(i = 0; i < 4; i++)
	{
		printf("%5.2f\t",*(p+i));
	}

	printf("\n");
	
	return 0;
}

七、野指针与空指针

(一)野指针

1. 定义:

访问了一个已经销毁或者访问受限的内存区域外的指针,这个指针就被称为野指针。

2. 野指针产生的场景:

  • 变量未初始化,通过指针访问该变量。

    int a;
    int *p = &a; // 此时 p 就是野指针
    ptf(*p); // 可以访问野指针,但是数据不安全
    
  • 指针变量未初始化。

    int *p = NULL; // 此时的 p 也是野指针
    ptf(*p); 
    
  • 指针指向的内存空间被(free函数)回收了。

  • 指针函数中直接返回了局部变量的地址。

  • 指针指向数组以外的地址(下标越界)。

3. 如何避免野指针:

写代码要养成两个习惯(通过编码规范尽可能避免)

  • 指针变量要及时初始化,如果暂时没有对应的值,建议赋值为NULL;

  • 数组操作(遍历,指针运算)时,注意数组长度,避免越界;

  • 指针指向的内存空间被回收,建议给这个指针变量赋值为NULL;

    int *p = (int*)malloc(10); // 动态内存分配
    free(p); // 动态内存释放
    p = NULL; // p 不指向任何空间
    
  • 指针变量使用之前要检查它的有效性(以后开发中要做非空校验)。

    int *p = NULL;
    if(!p)
    {
    	return -1;
    }
    

说明:NULL是空常量,它的值是0;这个NULL一般存放在内存中的0x00000000位置,这个地址只能存放NULL,不能被其他程序修改。

(二)空指针

空指针又被称为悬空指针:当一个指针的值是NULL,这个指针被称为空指针;对空指针访问会运行报错(段错误)

八、二级指针

1. 定义:

二级指针,又被称作多重指针,引用一级指针的地址,此时这个指针变量就得定义成二级指针。

int a = 10;
int *p = &a;
int **w = &p;

2. 定义格式:

数据类型 **变量名 = 指针数组的数组名或者一级指针的地址;
// 指针数组
int array = {1,2,3};
int *arr = {&arrar[0],&array[1],&array[2]}; // 指针数组

// 一级指针
int a = 10;
int *p = &a; // 一级指针

举例:

// 字符型指针数组
char *arr[3] = {"abc","aaa34","12a12"}; // 等效于:char arr[3][6] = {"abc","aaa34","12a12"}
// 定义二级指针并赋值(指针需要跟依赖的源同类型)
char **p = arr; // 正确


int arr[2][3] = {{1,2,3},{11,22,33}};
int **k = array; // 编译报错,数据类型不符(二维数组不等于指针数组)


int a = 90;
int *p = &a; // 一级指针
int **k = &p; // 正确,二级指针

结论

  1. 二级指针和指针数组是等效的,和二维数组不等效
  2. 二维数组和数组指针是等效的,和二级指针不等效

3. 二级指针的用法:

  • 如果是字符的二级指针,可以像遍历字符串数组一样遍历它
  • 如果是其他的二级指针,就需要解引用两次访问它所指向的数据

案例:

/**
* 二级指针案例:使用指向指针数据的指针变量。
*/

#include <stdio.h>

void fun1()
{
	char *name[]={"Follow me","BASIC","Great Wall","FORTRAN","Computer design"};
		
	// 定义一个二级指针
	char **p;

	// 定义循环变量
	int i;
	
	// 遍历指针数组
	for(i = 0; i < 5; i++)
	{
		p = name + i;
	
		printf("%s\n",*p);
	}	
	
	printf("\n");
}

void fun2()
{
	int arr[] = {6,8,9,66,88,99};

	// 创建一个指针数组
	int *arr1[] = {&arr[0],&arr[1],&arr[2],&arr[3],&arr[4],&arr[5]};

	int **p = arr1;

	// 遍历
	for(int i = 0; i < 6; i++)
	{
		// 方式1输出
		printf("%d ",**(p+i));
        
		// 方式2输出
		// printf("%d ",**p);
		// p++;
	}

	printf("\n");
}

void main()
{
	fun1();
	fun2();
}

九、main函数的函数原型

1. 定义:

main函数有多种定义格式,main函数也是函数,函数的相关结论对main函数也有效(也可以定义main函数的函数指针)。

2. main函数的完整写法:

int main(int argc,char *argv[]){}
int main(int argc,char **argv){}

3. 扩展写法:

int main(){}
int main(void){}
void main(){}
main(){}  ----- int main(){}
void main(void){}
int main(int a){}
int main(int a,int b,int c){}

说明:

  1. argc,argv是形参的名称,它们俩可以修改
  2. main函数的扩展写法有些编译器不支持,编译报警告
  3. argc和argv的常规写法:
    • argc:存储了参数的个数
    • argv:存储了所有参数的字符串形式
  4. main函数是系统通过函数指针的回调形式调用的

注意:

如果一个函数没有写返回值类型,这个函数的默认返回值是0

案例:

/**
* main函数
*/
#include <stdio.h>

int main(int argc,char **argv)
{
	int k;
    
	for(k=1; k < argc; k++)
    {
        printf("%s\n",argv[k]);
    }
	return 0;
}

十、常量指针与指针常量

1. 常量:

分为字面量和只读常量,字面量(就是我们平时直接操作的如:printf(12)、printf(“hello”));只读常量使用关键字const修饰,凡是被这个关键字修饰的变量,一旦赋值,值就不能改变。

2. 语法:

// 字面量举例,字面量是一种匿名的常量
printf(12);
printf("请输入一个数:\n");

// 只读常量
const int a = 10;
a = 21; // 编译错误,因为此时这个变量是只读常量,所以不可更改其值

3. 常量指针

定义:

常量的指针,本质是一个指针,指针指向的数据不能改变。

定义格式:

const 数据类型 *变量名;

举例:

const int *p; // p 就是常量指针

结论

  • 常量指针指向的数据不能被改变(不能解引用间接修改数据)。
  • 常量指针的地址可以改变。

应用场景:作为形式参数,实际参数需要给一个常量。

案例:

#include <stdio.h>

/* 常量指针 */
void test1()
{
	// 定义变量
	int a = 10;
    
	// 定义常量指针
	const int *p = &a;
    
	// *p = 100; // 编译报错,常量的值不能修改(常量指针指向地址空间的数值不能修改)
	printf("%d\n",*p);// 10
    
	int b = 90;
    
	p = &b; // 编译通过,常量的地址可以修改(常量指针指向的地址空间可以发生改变)
    
	printf("%d\n",*p);// 90
}

int main()
{
	test1();
}

4. 指针常量

  • 定义:指针的常量,指针的指向不能改变

  • 定义格式:

    数据类型* const 变量名;
    

    举例:

    int* const p; // 指针常量
    
  • 结论:

    1. 指针常量的指向不能被改变(不能给指针变量重新赋地址值)。
    2. 指针常量指向的数据可以改变。
  • 注意:指针常量在定义时就要赋值;不能先定义后赋值,否则编译报错。

  • 案例:

    /**
    * 常量指针与指针常量
    */
    
    #include <stdio.h>
    
    /* 指针常量 */
    void test2()
    {
    	// 定义变量
    	int a = 10;
        
    	// 定义指针变量
    	int* const p = &a;
        
    	// 错误写法:先定义,后赋值(编译报错)
    	// int* const p;
    	// p = &a;
        
    	// 间接取数据
    	pirntf("%d\n",*p);
        
    	// int b = 200;
    	// p = &b;// 编译报错,地址不能改变
        
    	*p = 200;
    	printf("%d\n",*p);// 200
    }
    
    int main()
    {
    	test2();
    }
    

常量指针常量

  • 定义语法:

    const 数据类型* const 变量名;
    

    举例:

    const int* const p; // 常量指针常量
    
  • 作用:p的指向不能被改动(地址),p指向的的 数据不能被改动(地址对应内存中存放的数据)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2054024.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【DA】《Augmentation for small object detection》

arXiv-2019 文章目录 1 Background and Motivation2 Related Work3 Advantages / Contributions4 Method4.1 Oversampling4.2 Augmentation4.3 Copy-Pasting Strategies 5 Experiments5.1 Datasets and Metrics5.2 Oversampling5.3 Augmentation5.4 Copy-Pasting strategies5.4…

Python入门级[ 基础语法 函数... ] 笔记 例题较多

本文是刚学习Python的笔记&#xff0c;当时使用的编辑器是交互式编程&#xff0c;所以很多代码可能在你们的编译器上面不能运行&#xff0c;我用快引用引起来了&#xff0c;还需要大家自己动手试一试。 内容涉及的比较简单&#xff0c;主要还是Python的语法部分&#xff1a;三…

Ubuntu20.04.4.LTS系统如何下载安装VirtualBox虚拟机?

&#x1f3c6;本文收录于《CSDN问答解惑-专业版》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收…

Solidworks二次开发:变螺距螺旋线

在帮助文档中&#xff0c;找到变螺距螺旋线的VBA代码&#xff0c;如下&#xff1a; --------------------------------------------------------------Preconditions: Verify that the specified part document template exists.Postconditions:1. Creates a variable-pitch h…

Nginx 学习之 配置支持 IPV6 地址

目录 搭建并测试1. 下载 NG 安装包2. 安装编译工具及库文件3. 上传并解压安装包4. 编译5. 安装6. 修改配置7. 启动 NG8. 查看 IP 地址9. 测试 IP 地址9.1. 测试 IPV4 地址9.2. 测试 IPV6 地址 IPV6 测试失败原因1. curl: [globbing] error: bad range specification after pos …

SQL— DDL语句学习【后端 9】

SQL— DDL语句学习 在数据管理的广阔领域中&#xff0c;SQL&#xff08;Structured Query Language&#xff09;作为操作关系型数据库的编程语言&#xff0c;扮演着举足轻重的角色。它不仅定义了操作所有关系型数据库的统一标准&#xff0c;还为我们提供了强大的工具来管理、查…

20240819用SDDiskTool_v1.72写IMG固件到256GB的TF卡后再用它给飞凌OK3588-C核心板刷机

20240819用SDDiskTool_v1.72写IMG固件到256GB的TF卡后再用它给飞凌OK3588-C核心板刷机 2024/8/19 10:35 1、精简的配置HDMI0为主显示屏的步骤&#xff1a; 在串口终端中启动到uboot阶段&#xff0c;按空格进入 显示配置模式。 按 2 进入&#xff1a;2:Display type 按 a 两次…

2、Future与CompletableFuture实战

Future与CompletableFuture实战 Callable与Future与FutureTask介绍Callable详解Future介绍FutureTask使用使用案例&#xff1a;促销活动中商品信息查询 Future的局限性 CompletableFuture使用详解应用场景创建异步操作runAsync&supplyAsync 获取结果join&get 结果处理w…

《亿级流量系统架构设计与实战》第十一章 Timeline Feed服务

Timeline Feed服务 一、概述1、分类2、功能 二、设计原理1、拉模式与用户发件箱2、推模式与用户收件箱3、推拉模式结合 三、关键技术1、内容与用户收件箱的交互&#xff08;推模式&#xff09;2、推送拆分子任务3、收件箱模型设计 内容总结自《亿级流量系统架构设计与实战》 一…

[linux#39][线程] 详解线程的概念

线程&#xff1a;是进程内的一个执行分支。线程的执行粒度比进程要细 什么是线程&#xff1f; • 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程 是“一个进程内部的控制序列” • 一切进程至少都有一个执行线程 • …

使用Virtio Driver实现一个计算阶乘的小程序——QEMU平台

目录 一、概述 二、代码部分 1、Virtio 前端 (1) User Space (2) Kernel Space 2、Virtio 后端 三、运行 QEMU Version&#xff1a;qemu-7.2.0 Linux Version&#xff1a;linux-5.4.239 一、概述 本篇文章的主要内容是使用Virtio前后端数据传输的机制实现一个计算阶乘的…

基于vue框架的爱喵星人服务平台设计与实现80sgi(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,猫食分类,猫粮信息,养护知识,流浪猫信息,申请领养,志愿者招募,申请加入,猫咪品种,团队活动 开题报告内容 基于Vue框架的爱喵星人服务平台设计与实现 开题报告 一、研究背景与意义 1.1 研究背景 随着社会的快速发展和人们生活水…

使用 onBeforeRouteUpdate 组合式函数提升应用的用户体验

title: 使用 onBeforeRouteUpdate 组合式函数提升应用的用户体验 date: 2024/8/15 updated: 2024/8/15 author: cmdragon excerpt: 摘要&#xff1a;本文介绍如何在Nuxt 3开发中使用onBeforeRouteUpdate组合式函数来提升应用用户体验。通过在组件中注册路由更新守卫&#xf…

Markdown导出为 Excel文件 Vue3

直接复制到单文件内即可使用 需要用到的插件 xlsx 0.17.5marked 14.0.0file-saver 2.0.5vue 3.4.29 直接SFC单文件内使用 <script setup> import {reactive} from vue; import xlsx from xlsx; import {marked} from marked; import {saveAs} from file-saver;const…

鸿蒙(API 12 Beta3版)【元数据(C/C++)】媒体相机开发指导

元数据&#xff08;Metadata&#xff09;是对相机返回的图像信息数据的描述和上下文&#xff0c;针对图像信息&#xff0c;提供的更详细的数据&#xff0c;如照片或视频中&#xff0c;识别人像的取景框坐标等信息。 Metadata主要是通过一个TAG&#xff08;Key&#xff09;&…

Linux基础知识学习(三)

3. Vim 编辑器 1> 定义 im 通过一些插件可以实现和IDE一样的功能&#xff01; vi 是老式的字处理器。 Vim是从 vi 发展出来的一个文本编辑器。代码补完、编译及错误跳转等方便编程的功能特别丰富&#xff0c;在程序员中被广泛使用。 键盘图为&#xff1a; ps &#xff1…

JMeter——设置全局变量和非全局变量

在用JMeter写接口case&#xff0c;遇到一种情况&#xff0c;接口1查看列表接口&#xff0c;接口2查看详情接口&#xff0c;接口2需要传入接口1列表的第一条数据的id。 如果这个id后续改变较多&#xff0c;可以使用非全局变量的设置方法&#xff1b; 如果这个id在整个case都比较…

vs2019使用setup打包exe学习记录

仅记录一下自己的学习过程&#xff0c;如果有问题&#xff0c;请多指正&#xff01; 开头注意&#xff1a;在打包之前一定要确保自己的工程是正常运行的&#xff0c;以及相关环境变量的配置是正确的&#xff0c;我后面就因为QT的环境变量问题报错。 我使用vs2019的QT项目写了…

本庄村果园预售系统的设计与实现bootpf

TOC springboot441本庄村果园预售系统的设计与实现bootpf 第1章 绪论 1.1 课题背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往往是人们思…

代码随想录算法训练营 | 动态规划 part06

322. 零钱兑换 给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 amount &#xff0c;表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额&#xff0c;返回 -1 。 你可以认为每种硬币的数量是…