c语言的指针

简介: c语言的指针

一、指针是什么

在计算机科学中, 指针 ( Pointer )是编程语言中的一个对象,利用地址,它的值直接指向

( points to )存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以

说,地址指向该变量单元。因此,将地址形象化的称为 “ 指针 ” 。意思是通过它能找到以它为地址

的 内存单元 。、

如下图所示,第一个图是我画的内存示意图,大概意思就是一个字节在内存中是从低到高的方式排列,也就是是说32位字节的情况下就是0x00000000-0xFFFFFFFF,32个字节的大小也就是2的32 次方,64位也就是2的64次方,由此可知指针的大小在不同位数的机器上是不同的,在32位的机器上就是4个字节,64位的就是8个字节。第二个图是我创建的一个变量a,然后取地址赋值给指针p,这时我把程序运行起来,然后在第三个图进行观看的他的内存地址,也就是&a,发现是0x001EFB80,然后我在第四个图地址那里输入了p然后发现和&a的值一摸一样。

所以,我们就可以得出结论指针的意思就是指向一个变量的地址。

二、指针和指针类型

众所周知,变量是有类型的,那么指针有没有类型呢?答案是肯定的,按照变量的类型来划分那么指针最少有和变量一样的类型,也就是最基本类型的指针,如下方代码,各种类型的变量都是有指针的。

所以我们就可以得出指针的定义就是类型+*+指针变量的名字。

char   * pc = NULL ;
int   * pi = NULL ;
short * ps = NULL ;
long   * pl = NULL ;
float * pf = NULL ;
double * pd = NULL ;

我们知道指针怎么定义的了,那一个指针是多大呢?

我们从下图可以看出,pa的地址是0x001AFA8C,pa+1的地址是0x001AFA90,我们发现他们之间差了四个字节,也就是说,int类型的指针+1就是跳过四个字节,那么我们干净看一下char类型指针+1是不是跳过了1个字节,经过我们查看,发现确实是跳过一个字节。

所以,由此可知,指针类型+几就是跳过当前类型指针的字节数乘上几,也就是int类型占据四个字节,+1就是跳过四个字节,+2跳过8个字节。同理,解引用的时候也就是只能操作这几个字节,int类型就是四个字节char类型就是1个字节。

字符指针和整形指针比较像也就是把字符串第一个字符的地址赋给指针,如下图所示用法和整形指针一样。

三、野指针

什么是野指针呢?

概念:

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量 ,在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一

个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知。

形成野指针的原因?

1、 指针未初始化

现在我们来一一了解,第一个未初始化这个就好理解了,如下代码就是典型的未初始化,我们在上文中提到过指针里面存放的是地址,他不是变量,没被初始化的时候,这里是随机值,没人知道他指向那里,所以我们一般定义指针时,却一时用不到,可以给他赋个NULL也就是0,这样他就不是野指针了。int main ()

{
int * p ; // 局部变量指针未初始化,默认为随机值
    * p = 20 ;
return 0 ;
}

2、指针越界访问

越界访问这个词相信都不陌生,我的第一反应就是数组嘛,那指针的越界访问是不是也可以这样理解?如下代码,我们都知道除了sizeof和&arr数组名就是首元素的地址,所以我们把首元素的地址赋值给p,p就相当于arr[0],当在for循环中p++在解引用后就相当于跳过一个整型,也就是arr[1],所以在for循环11次后,p的指向相当于arr[11],arr[11]我们没有创建啊,所以他就是野指针,这就是指针越界访问造成的野指针。

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

3、指针指向的空间被释放

这个的意思怎么说呢,我用个例子来解释吧,如下图,我们把a和b的值相加然后赋值给ret,在返回ret的地址,用指针c去接收,按照我们的想像好像没啥问题吧,在我们调试监控发现,c的是一个随机

数,为啥呢,因为,ret是个局部变量,在Add函数结束时就被销毁了,所以*c接收的地址也就不存在了,这也是一个野指针。

我们知道什么时野指针了,那么我们该如何规避野指针呢?

1、指针初始化

2、小心指针越界

3、指针指向空间释放即使置 NULL

4、指针使用之前检查有效性

我总结了这四点。

四、指针运算

指针运算就是指针的加减整数,指针减指针,指针的关系运算。

我们先来讲指针的加减运算,这个在前文中提到过,如图一即使指针的加法运算,我们把数组的首元素地址给p让p++就是地址++,而数组的数据存放就是依次存放的,所以能直接访问数组。如图二就是指针的减法,我把数组最后一个元素的地址给p再让他--,因为数据是整数类型的,所以自减1就是减四个字节也就是和数组arr[i]这样的写法一样,这个就是指针的加减法。

指针减指针,就如下图所示,我们发现他们两个相减就是数组第一个元素和最后一个元素之间的个数,由此我们可以得知指针减指针就是两个指针之间的元素个数。

五、指针和数组

指针和数组就是,突然发现好像上文中用它做实例了,哈哈。

如第二个图所示,arr数组内容在内存中就是这样的排序,在我们把arr首元素地址赋给p时,p通过++就可以访问数组内容了。

六、二级指针

指针变量也是变量,只要是变量就会在内存中开辟空间,就会有地址,所以二级指针就是存放一级指针变量地址的指针,定义方法如下面代码,有一级指针就会有二级指针、三级指针,所以这里就可以理解为套娃,就像下方代码,p是a的地址,pp是p的地址,ppp就是pp的地址,当然我们也可以通过二级指针、三级指针去找到a的数据。如下图就是我们解引用指针的方法,二级指针*pp就是一级指针的地址,**pp才是a的数据,三级指针同理也是这样的,所以这里的本质就是套娃。

int main()
{
    int a = 0;
    int* p = &a;
    int** pp = &p;
    int*** ppp = &pp;
    r

eturn 0;

}

七、指针数组与数组指针

指针数组?我们已经知道有整形数组,字符数组了,那么指针数组是什么东西?他是指针还是数组?是数组,但是是存放指针的数组,拿整形数组举例,我们都知道,整型数组里面是一串整形数据,所以我们指针数组里面也就是一串指针。如下图就是指针数组,在这个数组内里面存放都是整形指针,同理字符指针也是这样的。

数组指针乍一看和指针数组好像一样的,仔细一看好像还真是一样的,但实际不一样,数组指针是指针,指针数组是数组。如下面代码,仔细看p1是与数组先结合的在定义成指针的,所以是数组,p2是先与*结合在指向数组,所以是数组指针,意思就是指向数组的指针,[]的优先级要比*高,所以p2与*用()括起来。数组指针其实就是&arr,把这个数组的地址取出来赋给p2,代表这个指针储存的10个int的数据,如果+1就会跳过40个字节,也就是10个int类型的数据。

int * p1 [ 10 ];

int ( * p2 )[ 10 ];

举个例子:如下图和下方代码,从代码中我们可以看出来我定义了两个函数,一个是数组指针,一个是正常打印方式,众所周知二维数组名就是第一行的地址,所以我们把他出传参给p2函数,从打印结果来看,(*p2)[5]=arr[3][5],也就是p2+1就是arr数组的第二行。


void p1(int arr[3][5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
void p2(int (*arr)[5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
    p1(arr, 3, 5);
    p2(arr, 3, 5);
    return 0;
}

 八、数组传参和指针传参

数组传参:

请看下面这段代码是否能把参数传递过去?

先看test1,一维数组进行传参可以定义形参的数据大小,所以是可以的,test2很标准的接收,也是可以的,test3是用指针去接收的,也可以,test4这里不一样了,他是指针数组,但是接收也是用指针数组进行接收的,所以也是可以的,test5呢?他是利用一个 二级指针去接收的,我们创建的是指针数组,它的本质就是指针,就像上文说二级指针就说过,二级指针是用来存储一级指针的,而传过去的是数组名,接收也应该是用指针,所以这里用二级指针是完全OK的。

这就是一维数组的传参。

void test1(int arr[])//ok?
{}
void test2(int arr[10])//ok?
{}
void test3(int* arr)//ok?
{}
void test4(int* arr[20])//ok?
{}
void test5(int** arr)//ok?
{}
int main()
{
    int arr[10] = { 0 };
    int* arr2[20] = { 0 };
    test1(arr);
    test2(arr);
    test3(arr);
    test4(arr2);
    test5(arr2);
    return 0;
}

二维数组的传参,老规矩先放一段代码先来看看,从这些代码我们能不能直接看出来哪些错了呢。

首先test1肯定是对的,实参和形参一模一样,很标准的写法,test2这个就不对啦,因为二维数组可以忽略行,但是不能忽略列,你想想数组的数据在内存中是依次排放的,没有列去规定一行有几个,那不乱套了嘛,所以test2是错的,test3是对的,上面刚说可以省略行,所以test3是对的。tets4肯定也是错的,上文的一维数组就是这么接收的,二维怎么可能还是这样用,同理test7也是这原因,所以test4和test7都是错的,test5和test6乍一看都是对的,但是仔细看test5就是上文中说的指针数组和数组指针的区别,test5的arr是和后面的[]结合的,所以他是指针数组,如指针数组那里所画的图来看,指针数组里面是指针,这里补血上一题的一维数组那个定义的实参的数组就是指针数组所以这个错了,test6就没问题了,他是数组指针,接收的又是第一行的地址,所以他的意思就是(*arr)代表第一行的地址,大小是5个int类型,所以他是对的。

void test1(int arr[3][5])//ok?
{}
void test2(int arr[][])//ok?
{}
void test3(int arr[][5])//ok?
{}
void test4(int* arr)//ok?
{}
void test5(int* arr[5])//ok?
{}
void test6(int(*arr)[5])//ok?
{}
void test7(int** arr)//ok?
{}
int main()
{
    int arr[3][5] = { 0 };
    test1(arr);
    test2(arr);
    test3(arr);
    test4(arr);
    test5(arr);
    test6(arr);
    test7(arr);
    return 0;
}


一级指针的传参,说完数组的传参,该来到指针的传参了 ,一级指针传参比较简单,如下方代码,就是创建一个指针进行接收就可以了,就如下方代码。

void print ( int * p , int sz )
{
int i = 0 ;
for ( i = 0 ; i < sz ; i ++ )
{
printf ( "%d\n" , * ( p + i ));
}
}
int main ()
{
int arr [ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 };
int * p = arr ;
int sz = sizeof ( arr ) / sizeof ( arr [ 0 ]);
// 一级指针 p ,传给函数
print ( p , sz );
return 0 ;
}

二级指针的传参 ,如下方代码就是用一个二级指针接收就可以了,感觉越来越像套娃了,哈哈。下方代码就是把自己定义的二级指针传进行打印,还测试了一级指针取地址是不是和二级指针一样。


void test ( int** ptr )
{
printf ( "num = %d\n" , ** ptr );
}
int main ()
{
int n = 10 ;
int* p = & n ;
int ** pp = & p ;
test ( pp );
test ( & p );
return 0 ;
}

九、函数指针、函数指针数组、指向函数指针数组的指针

我们所创建的函数也是需要在地址上创建的,所以我们用什么来去接收函数指针呢?

没猜错就是套娃,先来看看我们需要一个指针也就是(*p),其次这个指针是函数,就是(*p)(),再然后看看函数的形参类型和函数需不需要返回值,返回值类型,所以就是int (*p)(int,int),就是下面代码的函数指针。


int test(int a, int b)
{
    return 0;
}
int main()
{
    int (*p)(int, int) = &test;
    return 0;
}

好,我们知道什么是函数指针了,那么根据套娃原则我们来看一下什么是函数指针数组是怎么定义的,首先拆开看下函数指针,数组,好首先定义int (*p)(int,int)这是函数指针,加上数组就是int (*p[5])(int,int),好这就是函数指针数组,通俗易懂,哈哈哈,这里我是写了一个通讯录的代码,来测试函数指针数组的可用,来一起看看代码把。

1、没用函数指针数组写的

int Add(int a,int b)
{
    return a + b;
}
int Sub(int a, int b)
{
    return a - b;
}
int Mul(int a, int b)
{
    return a * b;
}
int Div(int a, int b)
{
    return a / b;
}
void menu()
{
    printf("****************************\n");
    printf("******1、Add  2、Sub********\n");
    printf("******3、Mul  3、Div********\n");
    printf("******0、exit       ********\n");
    printf("****************************\n");
}
int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    do
    {
        printf("*************************\n");
        printf(" 1:add           2:sub \n");
        printf(" 3:mul           4:div \n");
        printf("*************************\n");
        printf("请输入:");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = Add(x, y);
            printf("ret = %d\n", ret);
            break;
        case 2:
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = Sub(x, y);
            printf("ret = %d\n", ret);
            break;
        case 3:
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = Mul(x, y);
            printf("ret = %d\n", ret);
            break;
        case 4:
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = Div(x, y);
            printf("ret = %d\n", ret);
            break;
        case 0:
            printf("已退出计算器\n");
            break;
        default:
            printf("请重新输入\n");
            break;
        }
    } while (input);
    return 0;
}

2、使用函数指针数组的


int Add(int a,int b)
{
    return a + b;
}
int Sub(int a, int b)
{
    return a - b;
}
int Mul(int a, int b)
{
    return a * b;
}
int Div(int a, int b)
{
    return a / b;
}
void menu()
{
    printf("****************************\n");
    printf("******1、Add  2、Sub********\n");
    printf("******3、Mul  3、Div********\n");
    printf("******0、exit       ********\n");
    printf("****************************\n");
}
//
int main()
{
    int(*arr[])(int,int) = { 0,Add,Sub,Mul,Div };
    int input = 0;
    int a = 0;
    int b = 0;
    do
    {
        menu();
        printf("请输入>");
        scanf("%d", &input);
        if (input == 0)
        {
            printf("已退出计算器\n");
        }
        if (input >= 1 && input <= 4)
        {
            printf("请输入两个操作数>");
            scanf("%d %d", &a, &b);
            int ret = arr[input](a, b);
            printf("%d\n", ret);
        }
        else
        {
            printf("请重新输入\n");
        }
    } while (input);
    return 0;
}

上面就是使用函数指针数组的与不使用的对比,可以大大的优化代码的效率,减少代码的书写。


下面来说指向函数指针数组的指针 ,顾名思义就是指向函数指针数组的一个指针,老方法,我们为您直接套娃,先写出一个函数指针数组int (*p[10])(int,int),那么我们怎么指向它呢,首先类型要一致,所以也是函数指针数组int (*pp[10])(int,int),我们直接套娃,int  (*(*pp[10]))(int,int),嘿一个新鲜出炉的指向函数指针数组的指针写完了。

十、回调函数

什么是回调函数?

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一

个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该

函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或

条件进行响应。

我们这里使用了一下qsort排序,这个库函数,他就是利用了回调函数才能让代码在任何类型都可以使用,这里我是用了两种去测试,第一个就是正常排序一串数组,从函数类型来看void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );base就是我们需要排序的起始地址,也就是数组名,num就是数据大小,也就是我们常说的数组大小,width就是宽度,也就是我们要排序类型的字节数,例如int就是4,后面就是一个比较类型函数,输入的e1和e2就是代表我们按照这样去比较,返回值是int类型根据msdn里面所写大如下表就是排序的方式,小于0就是e1小于e2,等于0就是e1等于e2,大于0就是e1大于e2,我们只需要根据这个写个对应的代码,就能实现qsort排序,这个函数是不是很厉害。当然我们还利用结构体去测试了代码,也是可以的。

我们自己根据这个函数写了个冒泡排序也适用各种类型,如下文冒泡排序的代码,这个代码中难点我觉得是如何实现函数的传参以及进行比较,我最后采用的把类型强制转成char*,因为char*是一个字节,所以我想进行下一个数据的排序时,只需要加上width也就是宽度,传递进来的宽度和数据是一样的,也就是int类型还是4,我就用这个乘上i,例如第二次排序,就是char*+4*2,刚好对应我们想要的数据的地址,这样我们也就实现了如何去寻找地址,第二个数据也是这样操作就可以了,不过冒泡排序,只需要比第一个数据大char*+4*(2+1)就行了, 排序就比较简单了,因为时char*类型,只需要根据传送过来的两个数据进行比较,然后交换就行了,这样我们就实现了冒泡排序模拟qsort排序了。

< 0 elem1 less than elem2
0 elem1 equivalent to elem2
> 0 elem1 greater than elem2

1、qosrt排序

qsort排序

int com_int(const void* e1, const void* e2)
{
    return (*(int*)e1) - (*(int*)e2);
}
void print(int arr[], int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
}
int main()
{
    int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr,sz,sizeof(arr[0]),com_int);
    print(arr, sz);
    return 0;
}
/分割线//
struct stu
{
    char a[20];
    int b;
    double c;
};
int com_int_age(const void* e1, const void* e2)
{
    return ((struct stu*)e1)->b - ((struct stu*)e2)->b;
}
int com_int_name(const void* e1, const void* e2)
{
    return strcmp(((struct stu*)e1)->a, ((struct stu*)e2)->a);
}
int main()
{
    struct stu arr[3] = {{"zhangsan",20,88.8},{"lisi",16,99.9},{"wangwu",23,66.6}};
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr,sz,sizeof(arr[0]),com_int_age);
    //print(arr, sz);
    return 0;
}

2、冒泡排序

int com_int(const void* e1, const void* e2)
{
    return (*(int*)e1) - (*(int*)e2);
}
void Swap(char* bu1, char* bu2, int width)
{
    int i = 0;
    for (i = 0; i < width; i++)
    {
        char tmp = *bu1;
        *bu1 = *bu2;
        *bu2 = tmp;
        bu1++;
        bu2++;
    }
}
void bubble_sort(void* base, int num, int width, int(__cdecl* cmp)(const void* e1, const void* e2))
{
    int i = 0;
    for (i = 0; i < num; i++)
    {
        int j = 0;
        for (j = 0; j < num - 1 - i; j++)
        {
            if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
            {
                Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
            }
        }
    }
}
void print(int arr[], int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
}
int main()
{
    int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz,sizeof(arr[0]), com_int);
    print(arr, sz);
    return 0;
}
目录
相关文章
|
2月前
|
存储 C语言
【C语言篇】深入理解指针3(附转移表源码)
【C语言篇】深入理解指针3(附转移表源码)
42 1
|
2月前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
52 0
|
12天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
2月前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
20 2
|
2月前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
2月前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
2月前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
2月前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
2月前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
2月前
|
C语言
C语言指针(3)
C语言指针(3)
14 1