深入理解指针

基本概念

指针与地址

简要说明

        在简单学习c语言后,我们很容知晓内存单元的编号==地址==指针。

        而指针变量是用来存放地址的,存放在指针变量中的值都会理解成地址。我们可以通过取地址符号(&)来获得地址,从而将其存储在指针变量中。

拆解指针类型

        我们可以看见指针的类型有非常多,如int*,char*,float*等等,那我们应该如何理解它?

        如我们先定义一个指针变量int* p,其中*是在说明p是指针变量,而前面的int是说明p指向的是整形(int)类型的对象。

        同理,char* p,*是说明p是指针变量,而char是说明p指向的字符型(char)类型的对象。

解引用操作符

        我们已经知晓指针变量是用来存贮地址的,那我们怎样使用保存在其的地址呢?

        如同于现实生活,我们可以通过地址找到一个特定的房子,在c语言中我们可以通过地址来找到特定的变量,而这里就要用到解引用操作符(*)。

        举个例子,我们定义一个变量a为10,然后通过地址来使a的值改变

int main()
{
    int a=10;
    int* p=&a;
    *p=0
    return 0;
}

        我们可以用编译器编译一下,就会发现a的值被改变了。 

指针变量的大小

        如同其他变量一样,指针变量也有自己的大小。对此我不多加赘述,直接写上结论。

        1.32位平台下地址是32个bit位,指针变量大小是4个字节

        2.64位平台下地址是62个bit位,指针变量大小是8个字节

        3.注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

指针类型变量的意义

        虽然不同类型的指针的大小是一样的,但是它们的含义不同。

        不同类型的指针在解引用时的权限不同,这决定了它们一次能操作多少个字节。

        比如char*指针的解引用只能访问一个字节,而int*指针的解引用能访问四个字节。

指针运算

指针+-整数

        指针+-整数在数组中比较常见到。因为数组在内存中是连续存放的,只要知道第一个元素的地址,就能顺藤摸瓜找到后面的元素。

        如:

#include <stdio.h>
#include <string.h>
int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    int i=0,sz=sizeof(arr)/sizeof(arr[0]);
    int* p=arr;
    for(i=0;i<sz;i++)
    {
        printf("%d",*(p+i);
    }
return 0;
}
指针-指针

        指针-指针一般用来确认两个指针之间相差的元素。

        拿自我实现strlen函数来举例:

int my_strlen(char* s)
{
    char* p=s;
    while(*p)
    {
        p++;
    }
    return p-s;
}
指针的关系关系运算
int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
    int *p=&arr[0];
    int i=0;
    int sz=sizeof(arr)/sizeof(arr[0]);
    while(p<arr+sz)
    {
        printf("%d ",*p);
        p++;
    }
    return 0;
}

void*指针

        在指针类型中,有一种特殊的类型为void*指针,可以理解为无类型的指针。它可以接收任意类型的指针,但无法直接进行解引用以及指针的运算(这里可以用强制类型转换来解决)

        ⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。

const修饰指针

        变量是可以修改的,但有的时候我们想让变量不被修改,此时就可以用到const。

const修饰变量

        举个简单的例子

int main()
{
    int a=10;
    a=20;//a是可以被改变的
    const int b=10;
    b=20;//b是不可以被修改的
    return 0;
}

        注:被修饰的变量它的值虽然不能被修改,但其本质还是变量,并不会因为const修饰而变成常量。 

const修饰指针变量

        指针变量与其他变量有点小小的不同,const放在*的左边和放在*的右边意义是不一样的

int const *p//const放在*的左边修饰
int *const p//const放在*的右边修饰

        让我们直接上结论:
        1.const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。

        2.const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。 

野指针

        概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

野指针成因

指针未初始化

int main()
{
    int* p;
    p=20;
    return 0;
}

指针越界访问

int main()
{
    int arr[10];
    int *p=arr;
    for(int i=0;i<11;i++)
    {
        *(p+i)=i;
        //当i=11时,此时指针指向的位置超出数组的范围
        //此时便为野指针
    }
    return 0;
}

指针指向的空间释放

int* test()
{
    int n=10;
    return &n;
}
int main()
{
    int* p=test();
    printf("%d",*P);
    //因为n为局部变量,出test函数时便被销毁,此时
    //p指向的地址已被释放,此时p为野指针
    return 0;
}
如何规避野指针

指针初始化

        养成好习惯 ,在定义指针变量时及时初始化,哪怕不知道指针要指向哪里也先置空

int main()
{
    int *P=NULL;
    return 0;
}

        在用free释放完指针时,也应该及时置空。

int main()
{
    int *p=10;
    free(p);
    p=NULL;
    return 0;
}

小心指针越界

        ⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

        就好像没有经过房主的邀请,是不能随意进入别人房间的。

避免返回局部变量的地址

        就如同之前举过的例子来说明,不要返回局部变量的地址。

int* test()
{
    int n=10;
    return &n;
}
int main()
{
    int* p=test();
    printf("%d",*P);
    //因为n为局部变量,出test函数时便被销毁,此时
    //p指向的地址已被释放,此时p为野指针
    return 0;
}

assert断言

        在函数进行传参时,我们要避免出现传空指针的情况等,以免程序出问题报错。

        在此我们可以运用assert来进行暴力检查。assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。

assert(P);

        上面这行代码就是验证p是否为空指针,若不是则程序继续运行,若为空指针程序立刻停止运行并报错。 

指针的使用和传址调用

strlen的模拟实现

        对指针进行进一步了解后,我们便可以运用之前学习的知识来模拟实现strlen函数

int my_strlen(const char* str)
{
    int count=0;
    assert(str);
    while(*str)
    {
        count++;
        str++;
    }
    return count;
}
传值调用和传址调用

        函数有两种调用方式,一种为传值调用,另一种为传址调用。

        我们将传值过去的变量成为实参,而被调用函数用来接收数据的变量成为形参。

        在传值调用中,实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。

        举个例子,如我们写个交换a,b两个变量的值的函数

void Swap(int a,int b)//这里的a,b为形参
{
    int c=a;
    a=b;
    b=c;
}
int main()
{
    int a=1,b=2;
    Swap(a,b);//这里的a,b为实参
    printf("%d %d",a,b);
    return 0;
}

        程序运行后,我们很容易可以发现,a和b的值并没有发生改变。所以传值调用形参是无法改变实参的值的。

        想要形参改变实参,我们就要用到传址调用

void Swap(int* a,int* b)//这里的a,b为形参
{
    int c=*a;
    *a=*b;
    *b=c;
}
int main()
{
    int a=1,b=2;
    Swap(&a,&b);//这里的a,b为实参
    printf("%d %d",a,b);
    return 0;
}

        这样一来,程序运行后,便可以发现a和b的值发生交换了。 

指针与数组

数组名的理解

        一般情况下,数组名就是首元素的地址,但却有两个例外:

         sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表示整个数组,计算的是整个数组的大小,单位是字节

         &数组名,这里的数组名表表示个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)

使用指针访问数组

        先上结论,arr[i]=*(arr+i),然后再举个由指针来访问数组的例子:

int main()
{
    int arr[10]={0};
    int sz=sizeof(arr)/sizeof(arr[0]);
    int sz=0;
    for(int i=0;i<sz;i++)
        scanf("%d",p+i);
    for(int i=0;i<sz;i++)
        printf("%d ",p[i]);
    return 0;
}

一维数组传参的本质

        我们已经知晓数组名是数组首元素的地址,那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组首元素的地址。

        所以当我们在函数内部用sizeof计算数组的长度时,sizeof(arr) 计算的是⼀个地址的大小(单位字节)而不是数组的大小(单位字节)

void test(int arr[10])
{
    int sz=sizeof(arr)/sizeof(arr[0]);
    printf("%d",sz);//此处打印出来的值为1
}

        正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

        所以一维数组传参,形参部分可以写成数组形式也可以写成函数形式。

冒泡排序

        在了解一维数组传参后,我们来写一个冒泡排序来巩固一下所学知识

void Bubble(int arr[],int sz)
{
    int flag=0,i=0,j=0;
    for(i=0;i<sz-1;i++)
    {
        flag=0;
        for(j=0;i<sz-1-i;j++)
        {
            if(arr[j]>arr[j+1])
            {    
                flag=1;
                int tmp=arr[j];
                arr[j]=arr[j+1];
                arr[j+1]=tmp;
            }
        }
        if(flag==0)
            break;
    }
}
int main()
{
    int arr[10]={2,31,5,3,74,542,213,23,75,24};
    int sz=sizeof(arr)/sizeof(arr[0]);
    Bubble(arr,sz);
    for(int i=0;i<sz;i++)
        printf("%d ",arr[i]);
    return 0;
}

二级指针

        指针变量也是变量,是变量就有地址,而二级指针就是存放指针变量的地址的变量。

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

        二级指针有两个运算 

        一个是通过解引用找到指针p

int a=10;
int *p=&a;
int **ptr=&p;
*ptr=&a;//等价于p=&a;

        第二个是通过先解引用找到指针p,然后再对p解引用找到a 

int a=10;
int *p=&a;
int **ptr=&p;
**p=20//本质上是a=20;

指针数组

        存放整形的数组,称为整形数组;存放字符的数组,称为字符数组

        而存放指针的数组,则被称为指针数组。指针数组的每个元素都是⽤来存放地址(指针)的,它的每个元素是地址,又可以指向一块区域

指针数组模拟二维数组

int main()
{
    int arr1[5]={1,2,3,4,5};
    int arr2[5]={2,3,4,5,6};
    int arr3[5]={3,4,5,6,7};
    int* p[3]={arr1,arr2,arr3};
    int i=0,j=0;
    for(i=0;i<3;i++)
    {
        for(j=0;j<5;j++)
            printf("%d ",p[i][j]);
    }
    return 0;
}

        parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的。

各类指针

字符指针变量

        在指针的类型中,有一类指针变量为字符指针char*

        它一般有两种用法,一种为指向单个字符:

char a='a';
char* p=&a;

        还有另外一种用法,即为指向一串字符串:

char* p="hello,world!";

         注意,这行代码并不是把字符串“hello,world!"放到字符指针p里了,实际上字符指针p包含的是这串字符串首字母的地址。

        而这里有一道经典的例题与之相关:

#include <stdio.h>
int main()
{
    char str1[] = "hello world.";
    char str2[] = "hello world.";
    const char *str3 = "hello world.";
    const char *str4 = "hello world.";
    if(str1==str2)
        printf("str1 and str2 are same\n");
    else
        printf("str1 and str2 are not same\n");
    if(str3==str4)
        printf("str3 and str4 are same\n");
    else
        printf("str3 and str4 are not same\n");
    return 0;
}

        答案很简单,因为str1和str2是两个不同的数组,所以它们指向的空间不同 ;

        而str3和str4指向的是同一个字符串,所以str3和str4指向的空间相同

数组指针变量

数组指针的定义

        数组指针变量是:存放的是数组的地址,能够指向数组的指针变量

        注意区分数组指针变量与指针数组变量

int *p[10];    //p为指针数组
int (*p2)[10]; //p2为数组指针
 数组指针的初始化

        如果我们想要将数组的地址存放在数组指针中,我们要运用&数组名

int arr[10];
int (*p)[10]=&arr;//此处&arr为取出整个数组的地址

        其中,int表示的是p指向的数组的元素类型

                   p为数组指针变量名

                   [10]为p指向的数组元素的个数

二维数组传参的本质

        首先我们先了解一下二维数组。二维数组可以看作每个元素为一维数组的数组,所以二维数组的首元素即为一维数组,及二维数组的数组名表示的为一维数组的地址。

        所以二维数组在传参时所传递的地址即为第一行这个一位数组的地址,所以对二维数组的传参可以这样进行编写:

void test(int arr[5],int row,int col)
{
    int i=0,j=0;
    for(i=0;i<row;i++)
    {
        for(j=0;j<col;j++)
            printf("%d ",arr[i][j]);
    }
}
int main()
{
    int arr[3][5]={{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
    test(arr,3,5);
    return 0;
}

函数指针变量

        通过类比前面的学习,我们可以得知函数指针变量是⽤来存放函数地址的,未来通过地址能够调用函数的。

函数指针的创建
int Add(int x,int y)
{
    return x+y;
}
int main()
{
    int (*p)(int x,int y)=&Add//此处的x与y可以被删除
    return 0;
}

其中,int为p指向的函数的返回类型,p为函数指针变量名,

        (int x,int y)为p指向的函数的参数的类型和个数的交代 

函数指针的使用
int Add(int x,int y)
{
    return x+y;
}
int main()
{
    int (*p)(int x,int y)=&Add;
    printf("%d",(*P)(2,3));//用法1
    printf("%d",p(2,3));//用法2
    return 0;
}
typedef关键字

        typedef是用来类型重命名的,将复杂的变量名称简单化

        如将 void(*)(int) 类型重命名为 pfun_t

typedef void(*pfun_t)(int);

函数指针数组

        把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组。

int (*P[3])();

转移表

        学习了函数指针数组,我们可以将其运用。如制作转移表。

        举一个实现计算器的例子:

        

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int Del(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{
	int input = 0;
	int (*p[5])(int x, int y) = { 0,Add,Del,Mul,Div };
	do
	{
		printf("1.Add   2.Del\n");
		printf("3.Mul   4.Div\n");
		printf("0.Eixt\n");
		printf("请输入你的选项:\n");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			int x=0,y=0;
			scanf("%d%d", &x, &y);
			int ret = (*p[input])(x,y);
			printf("结果为:%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出计算机:\n");
			break;
		}
		else
			printf("输入错误,请重新输入\n");
	} while (input);
	return 0;
}

回调函数

        首先来了解回调函数的概念:回调函数就是⼀个通过函数指针调用的函数。

        如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被用来调用其所指向的函数时,被调的用函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

qsort使用举例

用qsort函数排序整形数据
int sort_int(const void* s1, const void* s2)
{
	return *(int*)s1 - *(int*)s2;
}
int main()
{
	int arr[10] = { 2,443,123,344,124,56,23,41,367,36 };
	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), sort_int);
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}
用qsort函数排序结构数据
struct Stu
{
	char name[20];
	int age = 0;
};
int sort_by_name(const void* s1, const void* s2)
{
	return strcmp(((struct Stu*)s1)->name, ((struct Stu*)s2)->name);
}
int main()
{
	struct Stu s[3] = { {"zhangsan",12},{"lisi",23},{"wangwu",21} };
	qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), sort_by_name);
	for (int i = 0; i < 3; i++)
	{
		printf("%s ", s[i].name);
	}
}

用冒泡模拟实现qsort

void Swap(char* s1, char* s2,int size)
{
	while (size)
	{
		char tmp = *s1;
		*s1 = *s2;
		*s2 = tmp;
		s1++;
		s2++;
		size--;
	}
}
void BubbleSort(void* arr, size_t nums, size_t size, int(*cmp)(const void* s1, const void* s2))
{
	int i = 0, j = 0;
	for (i = 0; i < nums - 1; i++)
	{
		for (j = 0; j < nums - 1 - i; j++)
		{
			if ((cmp((char*)arr + j * size, (char*)arr + (j + 1) * size)) > 0)
			{
				Swap((char*)arr + j * size, (char*)arr + (j + 1) * size, size);
			}
		}
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值