C语言 | 指针 | 野指针 | 数组指针 | 指针数组 | 二级指针 | 函数指针 | 指针函数

文章目录

        • 1.指针的定义
        • 2.指针的加减运算
        • 3.野指针
        • [4.指针 & 数组 & 传参 & 字符数组](#4.指针 & 数组 & 传参 & 字符数组)
        • [5.数组指针 & 指针数组](#5.数组指针 & 指针数组)
        • 6.二级指针
        • [7.指针函数 & 函数指针 & 回调函数](#7.指针函数 & 函数指针 & 回调函数)
        • [8.函数指针数组 & 指向函数指针数组的指针](#8.函数指针数组 & 指向函数指针数组的指针)
1.指针的定义

指针 是内存中一个最小单元的编号,也就是地址。平常口语中所说的指针,通常指的是指针变量,是用来存放内存地址的变量。指针的大小在32位平台是4个字节,在64位平台是8个字节。可以通过sizeof求得指针的大小。

cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 5;
    char c = 'a';

    int* ptra = &a;
    char* ptrc = &c; 

    printf("%ld\n",sizeof(ptra));
    printf("%ld\n",sizeof(ptrc));

    return 0;
}

我的系统64位的Ubuntu系统所以输出8。从上面的代码可以看出用 type + * 定义一个指针变量,而&(取地址) 可以取得变量的地址。另外输出ptraptrc的结果都一样,难道它们的类型一样。不是的!ptra的类型是int*用来接收int类型变量的地址,而ptrc的类型是char*用来接收char类型变量的地址。对指针赋值的时候类型要匹配!

可以定义指针,那么如何取出指针的指向的内容呢,其实它和定义指针用的是同一个操作符 * (解引用操作符或间接访问操作符)指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节),char* 指针的解引用就只能访问1个字节,int*指针的解引用就只能访问4个字节。

cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 5;
    int* ptra = &a;

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

    return 0;
}
2.指针的加减运算

C语言程序运行指针的整数加减,但是不能乘除!其实加减有时候就够头疼了,要是能乘除那不得了了!指针的加减会根据指针的类型决定了指针向前或者向后走一步有多大(距离),这时候能直观体现指针是有类型的。

cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 5;
    char c = 'a';

    int* ptra = &a;
    char* ptrc = &c; 

    printf("prta : before = %p : after = %p\n",ptra,ptra + 1);
    printf("prtc : before = %p : after = %p\n",ptrc,ptrc + 1);

    return 0;
}

输出结果:输出的结果是按照16进制来展示的。

cpp 复制代码
prta : before = 0x7ffca65b0c74 : after = 0x7ffca65b0c78
prtc : before = 0x7ffca65b0c73 : after = 0x7ffca65b0c74
3.野指针

指针是很灵活的,但是有时候灵活就代表着容易出错,如果对指针操作不当那么很容易造成野指针。所谓的野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

1、比如我对int* 类型的指针加一然后解引用访问,就会出错!指针越界访问!(int 只有4个字节大小的空间属于它) 通常越界访问是对数组经行操作,所以要注意数组下标访问是否越界。

bash 复制代码
int main()
{
    int a = 5;
    int* ptra = &a;

    printf("*prta : before = %d : after = %d\n",*ptra,*(ptra + 1));

    return 0;
}

输出:有些环境就程序直接就崩溃了,这里就输出了随机值。

cpp 复制代码
*prta : before = 5 : after = 1747592652

2、指针未初始化

cpp 复制代码
int main()
{
    int* ptra;

    printf("*prta : before = %d : after = %d\n",*ptra,*(ptra + 1));
    return 0;
}

3、指针指向的空间释放。对malloc动态申请的内存free后再去访问,也会造成野指针。

如何避免野指针的出现: 1.指针初始化、2.小心指针越界、3.指针指向空间释放即使置NULL、4.避免返回局部变量的地址、5.指针使用之前检查有效性。

上面的方法都是依靠程序员有良好的编码习惯,而人有时候会犯错,所以在C++中引入了智能指针来避免野指针。

4.指针 & 数组 & 传参 & 字符数组

数组名首元素的地址,它和&数组名输出的结果是一样但是它们代表并不是一个意思,因为指针是有类型的。

cpp 复制代码
int main(int argc,char* argv[])
{
    int arr[5] = {1,2,3,4,5};
    printf("%p\n",arr);
    printf("%p\n",&arr);
    
    return 0;
}

输入结果

cpp 复制代码
0x7ffebf362450
0x7ffebf362450

如果对数组名 + 1和&数组名 + 1会发现它们输出的内容是不一样的。

cpp 复制代码
int main(int argc,char* argv[])
{

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

输出结果:

cpp 复制代码
0x7ffd97b8f650
0x7ffd97b8f650
0x7ffd97b8f654
0x7ffd97b8f664

指针的类型决定了指针向前或者向后走一步有多大(距离)。数组名 + 1会跳过4个字节因为它的类型是 int*,而&数组名 + 1的类型是int (*)[5] 整形数组指针,跳过了20个字节。如果尝试这样赋值 int * p = &arr,编译器会提醒你!

cpp 复制代码
test.c:374:15: warning: initialization of 'int *' from incompatible pointer type 'int (*)[5]' [-Wincompatible-pointer-types]
  374 |     int * p = &arr;

如果自动一个函数想要接收一个整形数组,不需要定义数组指针,只需要传递首元素的地址 就能访问到整个数组的元素。可以有三种方式。

方式一:

cpp 复制代码
void get(int* arr){}

方式二:这种方式需要和传入的数组的大小匹配,比较的麻烦,现实中定义函数的人也知道要定义多大。如果可以在函数中打印一下arr,发现就是一个指针的大小,而不是将整个数组传递。

cpp 复制代码
void get(int arr[5])
{
    printf("%ld\n",sizeof(arr));
}

方式三:直接用方式一就好了。

cpp 复制代码
void get(int arr[]){}

C语言没有像其他语言一样提供好用的String类型来处理字符串,它是通过char *指向 ""引起的字符串常量,或者通过字符数组(栈),再或者存放到malloc开辟的动态内存空间中。不管怎么说以\0标志结尾的C风格的字符串挺难用的。

那么char * (字符指针)和字符数组处理字符有什么区别呢:

cpp 复制代码
int main()
{
    const char* str1 = "hello world";
    const char* str2 = "hello world";
    char str3[] = "hello world";
    char str4[] = "hello world";

    printf("str1 = %p : str2 = %p\n",str1,str2);
    printf("str3 = %p : str4 = %p\n",str3,str4);
 	return 0;
}

输出结果

cpp 复制代码
str1 = 0x55a2615af008 : str2 = 0x55a2615af008
str3 = 0x7fff95f06ca0 : str4 = 0x7fff95f06cac

str1 和 str2 指向了存放在常量区中的同一个"hello world"字符串的首元素的地址,而str3 和str4在栈区开辟了两块不同的内存存放Hello world。这样做虽然浪费了,一定的内存,但是字符数组中存放的内容可以修改,而char* 指向的字符串常量不能被修改,通常会加上一个const来修饰。另外,从键盘上输入字符就需要用到字符数组来接收。

cpp 复制代码
int main()
{
    char str1[] = {'h','e','l','l','o'};
    char str2[] = {'h','e','l','l','o','\0'};
    char str3[] = "hello";

    printf("%ld\n",sizeof(str1));
    printf("%ld\n",sizeof(str2));
    printf("%ld\n",sizeof(str3));

    printf("%zd\n",strlen(str1));
    printf("%zd\n",strlen(str2));
    
    return 0
}

这里str1 定义并不是字符串,因为C字符串要以\0结尾,所以strlen输出什么都是未定义的。这里注意区分sizeof 是计算内存的大小,而strlen是计算字符串的长度。

5.数组指针 & 指针数组

数组指针是指针,而指针数组是数组用来存放指针。它们的定义非常的相似。
数组指针: int (*p1)[5],p1是一个指针变量,指向的是一个大小为5个整型的数组。
指针数组: int *p2[5],p2是数组名,可以存放着5个int * 类型的指针。

这里定义数组指针需要括号,因为 [] 的优先级要高于 *号的,加上()来保证p1先和*结合。使用指针数组可以接收&一维数组名。也可以接收二维数组中的元素,注意二维数组中的元素是一维数组的地址。

cpp 复制代码
void show(int(*p)[5],int cow,int col)
{
    for (int i = 0;i < cow ; i++) 
    {
        for (int j = 0; j < col; j++) 
        {
            printf("%d ",p[i][j]);
        }
        printf("\n");
    }
}

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

上面show函数打印了二维数组的值,数组通过 [] 访问更加清晰一些。在C++中通过重载 [] 也让容器在逻辑上顺序访问,哪怕它的底层空间不是连续的。这里的[] 和 * 是等价的。也可以用下面的方式,访问到二维数组的元素。

bash 复制代码
printf("%d ",*(*p + i) + j);
printf("%d ",*((*(p + i)) + j));

再来看一看,指针数组。来定义一个指针数组,并打印这些值。

cpp 复制代码
int main(int argc,char* argv[])
{
    char* names[5] = {"张三","李四","王五","赵六","田七"};

    for(int i = 0;i < 5;i++)
    {
        printf("%s\n",names[i]);
    }

    return 0;
}

上面的例子似乎没有什么挑战,那如果从键盘上输入五个人的名字,并输出呢,怎么做。就有一个关键点,指针数组中存放的是指针,如果从键盘上输入要用字符数组,然后赋值给指针。

cpp 复制代码
int main(int argc,char* argv[])
{
    char* names[5]; 

    for(int i = 0; i < 5;i++)
    {
        char name[25] = {0};
        printf("请输入一个名字:");
        scanf("%s",name);
        names[i] = name;
    }
    
    for(int i = 0;i < 5;i++)
    {
        printf("%s\n",names[i]);
    }

    return 0;
}

我们来使用指针数组完成一个有意思的东西,学过Linux的同学知道cat 命令是查看文本中的内容。我们自己编写一个程序也实现cat 命令完成的任务。没学过Linux也没有关系,知道了怎么编写命令,学的时候就不会被它唬住。

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc,const char* argv[])
{
    if(argc <= 1)
    {
        printf("Usage: %s <file1> [file2 ...]\n",argv[0]);
        return -1;
    }

    for(int i = 1;i < argc;i++)
    {
        FILE* file = fopen(argv[i],"r");
        if(file == NULL)
        {
            perror("文件不存在!\n");
            return -1;
        }

        char ch;

        while((ch = fgetc(file)) != EOF)
        {
            fputc(ch,stdout);
        }

        fclose(file);
    }

   return 0;
}

通过gcc编译并添加到环境变量中

bash 复制代码
☁  day2024_11_14 [master] ⚡  gcc test.c[自己的文件名] -o easy_cat
☁  day2024_11_14 [master] ⚡  sudo mv easy_cat /usr/bin
☁  day2024_11_14 [master] ⚡  easy_cat test.c
6.二级指针

指针变量也是变量,存放指针变量的指针称为二级指针。

cpp 复制代码
int main()
{
	int num = 5;
	int *p = &num;
	int **pp = &p;

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

	return 0;
}

输出结果

cpp 复制代码
  pp : 0x7fff341ddbc8 &p : 0x7fff341ddbc8
**pp : 5 *p : 5

那么二级指针的用处是什么呢。函数的传参的方式有传值和传地址,着两种方式,传值传递参数是指的拷贝,如果想改变两个变量的地址,那么就需要通过指针传递。同样的道理,我想改变指针变量的内容就需要指针变量的地址,那就是二级指针。

交换两个int 变量的值:

cpp 复制代码
void swap(int* a,int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

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

	printf("before a = %d,b= %d\n",a,b);
	swap(&a,&b);
	printf("after  a = %d,b= %d\n",a,b);
}

交换两个int* 指针变量指向的值:

cpp 复制代码
void swap(char** a,char** b)
{
	char* tmp = *a;
	*a = *b;
	*b = tmp;
}

当你想改变指针变量的指向的时候可以考虑使用二级指针,很多的数据结构中,二级指针都是很常见的。另外在巩固一下,如果要编写一个函数打印指针数组的值改怎么做呢。

cpp 复制代码
void showName(char** names,int size)
{
	for(int i = 0;i < size;i++)
	{
		printf("%s ",names[i]);
	}
	printf("\n");
}

int main()
{
	char* names[5] = {"张三","李四","王五","赵六","田七"};
	showName(names,5);
	
	return 0;
}

为什么需要二级指针接收呢,可以这么理解:names[0] 类型为char*&names[0]的类型为char**。数组名就是首元素的地址,name = &names[0]实参传递的是 char**类型,showName的形参类型也要用二级指针来接收。

7.指针函数 & 函数指针 & 回调函数

函数返回值类型是指针的函数称为指针函数,通常是返回堆空间的指针,避免返回栈上空间,出了作用域被销毁,访问一块销毁的空间。

cpp 复制代码
void* malloc(int size);
char* strcpy(char* dest,char* src);

函数指针的本质是指针,用来存放函数的地址,而函数名就是函数在内存中首元素的地址。如何判断函数的类型。如定义一个add函数:

cpp 复制代码
int add(int a,int b)

确认一个函数类型最快速的方法是,去掉函数名和函数的形参就是函数的类型。add 函数的类型是int (int,int)。然后添加一个 (* p)就可以定义一个函数指针了。

cpp 复制代码
#include <stdio.h>

int add(int a,int b)
{
	return a + b;
}

int main()
{
	int (*p1)(int,int) = add;
	int (*p2)(int,int);
	p2 = add;

	printf("%d\n",p1(1,2));
	printf("%d\n",p2(1,3));

	return 0;
}

定义的函数指针可以在定义的时候赋初值,也可以先声明在赋值。使用函数指针像函数一样使用即可。既然像函数一样使用,那干嘛要麻烦的定义函数指针呢?定义函数指针可以让函数以参数的方式传递,让声明和实现分离,提高了代码的模块化程度。 这就像其他如Java语言中定义接口,通过接口来调用实现接口的类。

下面实现了加减,如果发生乘除取模(整除)等添加另外的功能函数的类型一样都可以通过calculate去调用。

cpp 复制代码
#include <stdio.h>

int add(int a,int b)
{
	return a + b;
}

int sub(int a,int b)
{
	return a - b;
}

int calculate(int cal(int,int),int a,int b)
{
	return cal(a,b);
}

int main()
{
	printf("%d\n",calculate(add,5,10));
	printf("%d\n",calculate(sub,10,5));

	return 0;
}

回调函数callback,通常我们调用别人的函数API这个过程叫做call,而别人在他定义的函数中调用我们的函数这个行为就是callback。上面的例子中calculate 调用cal 这个动作就是callback。在C语言中qsort 就用到了callback 。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int int_cmp(const void* p1,const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

int main()
{
	int arr[] = {1,6,2,3,8,9,3};

	int size = sizeof(arr) / sizeof(arr[0]);

	qsort(arr,size,sizeof(int),int_cmp);
	for(int i = 0;i < size;i++)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");

	return 0;
}

那qsort大概是怎么实现的呢?使用一个冒泡排序,实现对任意类型数据的排序。

C语言中使用void* 来接收任意类型,但是void* 不能加减运算,但是类型又是不确定的,所以只能转成最小类型的指针char* ,按照一个字节来操作数据。比较的时候,只需要两个确定元素的地址,而char* 一次只能跳过一个字节,所以要根据size来确定跳几个字节。交换的时候是按照一个一个字节,交换内容,要交换size次。

cpp 复制代码
void _swap(void* p1,void* p2,int size)
{
	for(int i = 0;i < size;i++)
	{
		char ch = *((char*)p1 + i);
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = ch;
	}
}

void my_qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*))
{
	for(int i = 0; i < num - 1;i++)
	{
		for(int j= 0;j < num -1 -i;j++)
		{
			if(compar((char*)base + j * size,(char*)base + (j + 1) * size) > 0)
			{				
				_swap((char*)base + (j* size),((char*)base + (j+ 1)* size),size);
			}
		}
	}
}

int int_cmp(const void* p1,const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

int main()
{
	int arr[] = {5,1,2,3,8,9,3};

	int size = sizeof(arr) / sizeof(arr[0]);

	my_qsort(arr,size,sizeof(int),int_cmp);
	for(int i = 0;i < size;i++)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");

	return 0;
}
8.函数指针数组 & 指向函数指针数组的指针

用来存放函数指针的数组。在上面的calculate例子中,可以使用函数指针数组来存放函数指针。

cpp 复制代码
int add(int a,int b)
{
	return a + b;
}

int sub(int a,int b)
{
	return a - b;
}

int main()
{
	int (*p[2])(int,int) = {add,sub};
	for(int i = 0;i < 2;i++)
	{
		printf("%d\n",p[i](10,5));
	}
}

当然还可以定义一个指向函数指针数组的指针

cpp 复制代码
int (*(*pp)[2])(int,int) = &p;
相关推荐
卡卡_R-Python3 分钟前
训练误差or测试误差与特征个数之间的关系--基于R语言实现
开发语言·回归·r语言
禾乃儿_xiuer16 分钟前
《Python制作动态爱心粒子特效》
开发语言·python·生活·pygame·爱心代码·python表白·初学者入门
Fms_Sa28 分钟前
数据结构查找-哈希表(开发地址法+线性探测法)+(创建+查找+删除代码)+(C语言代码)
c语言·数据结构·散列表
析木不会编程29 分钟前
【数据结构】【线性表】静态链表(附C语言源码)
c语言·数据结构·链表
励志成为嵌入式工程师29 分钟前
数据结构(顺序栈——c语言实现)
c语言·数据结构
2401_8582861131 分钟前
97.【C语言】数据结构之栈
c语言·开发语言·数据结构·链表·
ModelBulider40 分钟前
十四、SpringMVC的执行流程
java·开发语言·后端·spring·springmvc
军训猫猫头1 小时前
36.矩阵格式的等差数列 C语言
开发语言·c++
CodeWithMe2 小时前
【C/C++】Lambda 用法
c语言·c++