C语言,初识指针

前言

        指针,存在于C语言中,用途广泛。有了它就可以直接使用地址找到存储数据的空间直接修改,或者使用。那么在学习指针之前我一直有一个疑惑,那就是为什么要有指针?函数的内容可以直接通过名字直接访问,为什么还需要绕一圈?这不是更复杂了吗?事实上指针给我们提供了许多便利,接下来就来具体介绍什么是指针,以及指针的作用。

一. 简单的指针

        指针类型:
        char* - 指向字符的指针
        short* - 指向短整型的指针
        int*-指向整型的指针
        float* -指向单精度浮点型的指针
        void*--无具体类型的指针

        除了这些指针之外,还有许多复杂的指针,将在之后的大题里讲到。

1. 指针的大小

        指针类型的大小在32位的计算机中是4个字节,在64位的计算机中是8个字节,是固定的。因此只要是指针他的大小就是4或者8。

2. 指针类型的意义

        既然指针的大小是固定的,那么为什么会有这么多类型都是指针呢?

        这是因为指针要访问空间的大小是由类型决定的,相当于原来的类型去掉1个‘*’号。比如说‘char*’类型解引用所访问的类型是‘char’,而‘int*‘类型访问的就是’int‘。他们分别访问1个字节和4个字节的空间,并且识别为个自类型。

        如果将’int*‘型的地址放到其他类型的地址处就会报错。例如:

int main()
{
    int a = 0;
    //char* p = &a;//err
    return 0;
}

        正确的方式应该存到对应的类型里,实在需要存储就需要强制类型转换。

3. const在指针中的作用

        const修饰的变量将无法使用变量改变,但是如果使用指针的话还是可以改变它。因为限定的是变量而不是指针。

int main()
{
    const int a = 0;
    int b = 20;
    const int* p = &a;
    p = &b;
    //*p = 10;
    printf("a = %d\n", a);
    return 0;
}

        

        这里还会报一个错,说明编译器在提醒你,’a'被‘const’修饰了但还是被改变了。

        如果想让指针‘p’无法修改‘a',那么也可以用const修饰它。

        如果‘const’在‘*’前面,那么‘*p’就无法再被解引用修改‘a',直的注意的是,虽然’*p‘无法再被修改,但是’p'里面所存的地址是可以修改的。

int main()
{
    const int a = 0;
    int b = 20;
    const int* p = &a;
    p = &b;
    //*p = 10;
    printf("a = %d\n", a);
    return 0;
}

        那么如果需要控制‘p'也不变,则需要将’const‘放在’*‘后面。

int main()
{
    const int a = 0;
    int b = 20;
    int* const p = &a;
    p = &b;
    *p = 10;
    printf("a = %d\n", a);
    return 0;
}

        如果需要控制’p‘和’*p'都不变,则需要分别在‘*’前后增加‘const’。

int main()
{
    const int a = 0;
    int b = 20;
    const int* const p = &a;
    p = &b;
    *p = 10;
    printf("a = %d\n", a);
    return 0;
}

4. 指针的运算

        指针也是可以通过加减来计算的。但是指针的计算结果并不是单纯的数学计算。

        例如‘int*’型的指针‘+1’的话,地址会向后偏移4位。而‘char*’类型‘+1’只会偏移1位。这是因为指针及偏移量是由指针所指向的类型的大小所决定的,int是4个字节的大小,char是1个字节。所以结果很明显了。


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

        pa+n将其转化为整形计算,相当于pa+n*sizeof(a)。其中‘pa’表示‘a’的地址,n表示常数。

5. 指针里内容打印

        (1)int*型

        打印可以分为2种方法,一种是类似于数组的写法,一种是指针解引用的写法。两种方法的到的结果都是一样的。

int main()
{
    int arr[]= {1,2,3,4,5,6,7,8,9,10};
    int* pa = arr;
    int i = 0;
    for(i = 0; i < 10; i++)
    {
        printf("%d ", pa[i]);
    }
    printf("\n");
    for(i = 0; i < 10; i++)
    {
        printf("%d ", *(pa + i));
    }
    printf("\n");
    return 0;
}

        pa[i]和*(p+i)的效果是等价的。

        (2)char*类型

int main()
{
    char arr[]= {'a','b','c','d'};
    char* pa = arr;
    char i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);
    for(i = 0; i < sz; i++)
    {
        printf("%c", pa[i]);
    }
    printf("\n");
    for(i = 0; i < sz; i++)
    {
        printf("%c", *(pa + i));
    }
    printf("\n");
    printf("%s\n", pa);
    return 0;
}

        

        ‘%s’是打印中比较特殊的一种,他专门用来打印字符串。它的用法是后面传的是地址,从这个地址开始向后打印字符串直到遇到‘\0’结束。这里没有放‘\0'也结束了,说明这个字符传后面刚好有个’\0‘。实际上如果不自己添加‘\0’的话,‘%s’会打印乱码出来。切记。

        (3)其他简单类型

        如果是其他类型如double或者float类型,只需要把打印的%d换成%lf和%f就可以。实际上在简单类型里会有所区别的只有char类型。

6. 指针的大小

        指针可以通过地址的位置来分别大小,相同类型的指针还可以进行+-运算。

        

        做差的时候相减得出的是中间间隔的元素个数。比如说这里相减的结果是1而不是4。

int main()
{
    int arr[]= {1,2,3,4,5,6,7,8,9,10};
    int* pa = arr;
    int* pb = &arr[1];
    if(pa > pb)
    {
        printf("pa = %p\n", pa);
    }
    else
    {
        printf("pb = %p\n", pb);
    }
    printf("pb - pa = %d\n", pb - pa);
    return 0;
}

        

        这里的pb>pa所以pa的地址没有打印。

7. 野指针

        野指针的意思就是指针指向的空间没有被软件使用,这个时候造成了非法访问。这种样子大概有2种情况。

        (1)未被初始化

int main()
{
    int* pa;
    *pa = 10;
    printf("%d\n", *pa);
    return 0;
}

        这种情况下修改‘*pa’会导致程序直接崩溃,‘pa’指向了一个随机的地方,如果这个地方指向的系统的模块可能会导致系统直接崩溃。为了避免这种情况,我们可以在开始的时候直接给pa赋值或者给一个NULL。

int main()
{
    int a = 10;
    int* pa = &a;
    *pa = 10;
    printf("%d\n", *pa);
    return 0;
}

  或者

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

        养成好习惯,这样程序就不会简单的出错。

        当然pa=NULL的时候,是不能使用,修改‘*p’的。

        (2)使用的空间还给了内存

        这种情况在于用指针在函数里指向了一块函数内的空间,函数结束了仍然使用该指针访问已经销毁了的函数空间,这样也会出错。这种情况同样出现于free()函数里。所以指针在使用完成之后最好将其置空。

8. 指针的作用

        既然可已使用变量自己修改自己,那为什么需要使用地址呢?

        这是因为函数的返回值只能返回一个内容,所以如果要在函数内部修改2个或者以上的变量就必须要用到指针。因为函数形参只是实参的一份临时拷贝,如果不使用指针将无法修改实参。例如交换两个数的值。

void Swap1(int a, int b)
{
    a = 10;
    b = 20; 
}

void Swap2(int* pa, int* pb)
{
    *pa = 10;
    *pb = 20; 
}

int main()
{
    int a = 0, b = 0;
    Swap1(a, b);
    printf("a = %d, b = %d\n", a, b);
    a = 0;
    b = 0;
    Swap2(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

        从上面的例子就能够体现指针的作用了。还有一句重要的话要送给大家,要在函数内修改外面的数,就需要用到该函数的指针。也就是说改int就用int*,改int*就用int**。依次类推。

二. 指针与数组的关系

1. 数组名的理解

       (1) sizeof(数组名)

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

int main()
{
    int arr[10] = {0};
    printf("%d\n", sizeof(arr));//整个数组的大小
    printf("%d\n", sizeof(arr[0]));//的一个元素的大小
    printf("%d\n", sizeof(arr + 1));//第二个元素地址的大小
    return 0;
}

        (2)&数组名

        这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是相同的)除此之外,任何地方使用数组名,数组名都表示首元素的地址。

int main()
{
    int arr[10] = {0};
    printf("arr     = %p\n", arr);
    printf("arr     = %p\n", arr + 1);

    printf("&arr[0] = %p\n", &arr[0]);
    printf("&arr[0] = %p\n", &arr[0] + 1);

    printf("&arr    = %p\n", &arr);
    printf("&arr    = %p\n", &arr + 1);
}

        可以看出来arr,&arr[0],&arr,它们指向位置是相同的,但是类型是不同的,这里的arr和&arr[0]是一样的,+1跳过了4个字节。但是&arr表示整个数组,+1跳过了40个字节。

2. 数组与指针的关系

        1.数组就是数组,是一块连续的空间 (数组的大小和数组元素个数和元素类型都有关系)
        2.指针(变量)就是指针(变量),是一个变量(4/8个字节)
        3.数组名是地址,是首元素的地址
        4.可以使用指针来访问数组

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

        

3. 函数形参中的指针和数组

        数组传参的时候,形参是可以写成数组的形式的但是本质上还是指针变量。

        (1)数组传参的本质是传递了数组首元素的地址,所以形参访问的数组和实参的数组是同一个数组的。
        (2)形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略掉数组大小的。

void Print(int arr[10])//等同于int *arr
{
    int sz = sizeof(arr) / sizeof(arr[0]);
    for(int i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

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

        

        所以sz的计算最好创建在原函数里面然后传到函数的形参上。

4. 指针数组

        指针数组,顾名思义就是存放指针的数组,数组的每一个元素都是同类型的指针。

int main()
{
    int arr1[] = {1,2,3,4,5};
    int arr2[] = {2,3,4,5,6};
    int arr3[] = {3,4,5,6,7};
    int* arr[] = {arr1,arr2,arr3};
    //打印数组
    int i = 0;
    for(i = 0; i < 3; i++)
    {
        int j = 0;
        for(j = 0; j < 5; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

        使用起来的结果,如上。“arr”就是指针数组。

5. char*指针的特殊用法

int main()
{
    char* p = "abcdef";
    *p = 'w';//出现异常
    printf("%s\n", p);
    return 0;
}

        说到特殊用法,就是用指针接收一个字符串的地址,如图所示。这样子写的字符串是可以被打印的,但是不能修改。因为这样使用的字符串不是存在栈区的,而是在静态区内。这就和计算机内存的分配有关了。计算机主要分为:栈区、堆区、静态区。还有一些其他的部分,但一般不能访问。

int main()
{
    char* p = "abcdef";
    //*p = 'w';//出现异常
    printf("%s\n", p);
    return 0;
}

        直接打印时可以的。除此之外,存放的地址也和数组的不同。

int main()
{
   char str1[] = "hello bit.";
   char str2[] = "hello bit.";

   const char* str3 = "hello bit.";
   const char* str4 = "hello bit.";

   if (str1 == str2)
       printf("str1 and str2 are same\n");//1
   else
       printf("str1 and str2 are not same\n");//2

   if (str3 == str4)
       printf("str3 and str4 are same\n");//3
   else
       printf("str3 and str4 are not same\n");//4

   return 0;
}

        对于以上代码,可以知道的是“str1”!=“str2”。但是“str3”和“str4”呢?

        结果如下,静态区相同的量只创建一次,所以“str3”==“str4”。

6. 数组指针

        数组指针和指针数组刚好相反,数组指针表示的是指向数组的指针。

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int* p1 = arr;
    //int*    int*
    int (*p2)[10] = &arr;
    //  int(*)[10]  int(*)[10]
    printf("%p\n", p1);
    printf("%p\n", p1 + 1);

    printf("%p\n", p2);
    printf("%p\n", p2 + 1);
    return 0;
}

        这里的“p2”就是数组指针,它指向的是整个数组。

        

        这一点从运行结果上也可以看出。需要注意的是指针的类型不同是不能被赋值的,虽然地址相同但是解引用所提取的空间大小不同。

void Print(int arr[3][5], int r, int c)
{
    int i = 0;
    for(i = 0; i < 3; i++)
    {
        int j = 0;
        for(j = 0; j < 5; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

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

        形参里的二维数组实际上也是个指针,如果改成一下形式运行结果不变。

void Print(int (*arr)[5], int r, int c)

        所以说数组与指针其实是有着密切关联的,具体细节需要多编程,多体会。

三. 函数与指针

1. 回调函数

        函数名本质上是一个地址,把这个地址赋给指针,用这个指针访问函数称为回调函数。

int (*p)(int, int) = Add;

        通过这样的定义可以获得Add函数的地址,直接访问Add。当然加上&在Add前也是一样的。

int Add(int x, int y)
{
    return x + y;
}

int main()
{
    int (*p)(int, int) = Add;
    printf("%d\n", p(3, 5));
    printf("%d\n", (*p)(3, 5));
    printf("%d\n", Add(3, 5));
    return 0;
}

        得到的结果如下。

        以上三种方案调用函数都是可行的。

2. 回调函数的作用

        介绍了回调函数的组成,那么接下来来讲讲回调函数的作用。

        回调函数的作用主要用在在函数里调用不固定的函数,这个说法就很明显了,比如在qsort()函数里就需要一个比较大小的函数,最后一个地方需要传一个函数名进去。因为不同种类的元素比较的方法不同,需要单独设计满足要求的函数在传函数名进去。

//void qsort(void* base, //指针,指向的是待排序的数组的第一个元素
//          size_t num,  //是base指向的待排序数组的元素个数
//          size_t size, //base指向的待排序数组的元素的大小
//           int (*compar)(const void*, const void*)//函数指针 -  指向的就是两个元素的比较函数
//          );


//void* 类型的指针是无具体类型的指针,这种类型的指针不能直接解引用,也不能进行+-整数的运算

//qsort 函数有实现者 - 
//qsort 函数的使用者 - 明确的知道要排序的是什么数据,这些数据应该如何比较,所以提供两个元素的比较函数
struct Stu
{
	char name[20];
	int age;
};

int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}

int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p1)->age-((struct Stu*)p2)->age;
}

void main()
{
	struct Stu arr[3] = { {"zhangsan", 20},{"lisi", 35},{"wangwu", 18}};
	int sz = sizeof(arr) / sizeof(arr[0]);
	//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age); 
}

        这样就可以方便排序,qsort()不需要针对不同的排序方式写不同的函数内容,只用把回调比较的函数就行。

四. sizeof()和strlen()

1. sizeof

        sizeof()用来计算里面内容所占的字节,在这之中有两个例外:

        (1)sizeof(数组名) - 数组名表示整个数组,计算的是整个数组的大小,单位是字节。

        (2)&数组名 - 数组名表示整个数组,取出的是整个数组的地址

        那么接下来举一些简单的例子来说明。

        为了方便比较strlen与sizeof的区别,这里的数组使用的是char类型。

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };

	printf("%d\n", sizeof(arr));//数组名单独放在sizeof内部了,计算的是数组的大小,单位是字节-6
	printf("%d\n", sizeof(arr + 0));//arr是数组名表示首元素的地址,arr+0还是首元素的地址,是地址就是4/8个字节
	printf("%d\n", sizeof(*arr));//arr是首元素的地址,*arr就是首元素,大小就是1个字节
	//*arr -- arr[0] - *(arr+0)
	printf("%d\n", sizeof(arr[1]));//arr[1] 是第二个元素,大小也是1个字节
	printf("%d\n", sizeof(&arr));//&arr 是数组地址,数组的地址也是地址,大小是4/8个字节
	//&arr -- char (*)[6]
	printf("%d\n", sizeof(&arr + 1));//&arr+1, 跳过整个数组,指向了数组后边的空间,4/8个字节
	printf("%d\n", sizeof(&arr[0] + 1));//第二个元素的地址,是地址就是4/8字节

	return 0;
}

        以上就是sizeof里面包含不同内容时的结果,有些复杂。

2. strlen

        对于strlen来说,就是从给的地址往后数,数到“\0”停止。需要注意的是strlen专门用在求字符串长度,其他的是不行的,而且一定传的是一个地址。

        (1)如果字符串中没有“\0”。

#include <string.h>

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", strlen(arr));//arr是首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机的
	printf("%d\n", strlen(arr + 0));//arr+0是数组首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机的
	printf("%d\n", strlen(&arr));//&arr是数组的地址,起始位置是数组的第一个元素的位置,随机值 x
	printf("%d\n", strlen(&arr + 1));//随机值 x-6
	printf("%d\n", strlen(&arr[0] + 1));//从第2个元素开始向后统计的,得到的也是随机值 x-1

	return 0;
}

        如果传的不是地址会被转化为地址,这个地址就相当于野指针,而电脑中有部分内存是无法被访问的,所以编译会出错。

	//printf("%d\n", strlen(*arr));//arr是首元素的地址,*arr是首元素,就是'a','a'的ascii码值是97
	//就相当于把97作为地址传递给了strlen,strlen得到的就是野指针, 代码是有问题的
	//printf("%d\n", strlen(arr[1]));//arr[1]--'b'--98,传给strlen函数也是错误的

        (1)如果字符串中有“\0”。

        替换调定义数组的代码换成如下代码。

char arr[] = "abcdef";

        其他打印的代码不变,结果会如何呢?

int main()
{
	char arr[] = "abcdef";
	printf("%d\n", strlen(arr));//6
	printf("%d\n", strlen(arr + 0));//arr首元素的地址,arr+0还是首元素的地址,向后在\0之前有6个字符
	printf("%d\n", strlen(&arr));//&arr是数组的地址,也是从数组第一个元素开始向后找,6
	                            //&arr -- char (*)[7]
	                            //size_t strlen(const char* s);
	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//5
	return 0;
}

        其实就是多了一个“\0”的结尾,那么字符串大小就是确定的了,当然strlen(&arr + 1)越界了,不算。

3. 二维数组中的sizeof

        这个玩意,及比一维数组难多了,什么时候表示元素,什么时候表示一行,什么时候表示整个数组。

        当然判断大小的分水岭在于里面的东西是不是指针,是指针就是4/8。不是指针再具体来看是什么就好了。

int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));//a是数组名,单独放在sizeof内部,计算的是数组的大小,单位是字节 - 48 = 3*4*sizeof(int)
	printf("%d\n", sizeof(a[0][0]));//a[0][0] 是第一行第一个元素,大小4个字节 
	printf("%d\n", sizeof(a[0]));//a[0]第一行的数组名,数组名单独放在sizeof内部了,计算的是数组的总大小 16 个字节
	printf("%d\n", sizeof(a[0] + 1));//a[0]第一行的数组名,但是a[0]并没有单独放在sizeof内部,所以这里的数组名a[0]就是
	//数组首元素的地址,就是&a[0][0],+1后是a[0][1]的地址,大小是4/8个字节

	printf("%d\n", sizeof(*(a[0] + 1)));//*(a[0] + 1)表示第一行第二个元素,大小就是4
	printf("%d\n", sizeof(a + 1));//a作为数组名并没有单独放在sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,也就是
	//第一行的地址,a+1,跳过一行,指向了第二行,a+1是第二行的地址,a+1是数组指针,是地址大小就是4/8个字节

	printf("%d\n", sizeof(*(a + 1)));//1.a+1是第二行的地址,*(a+1)就是第二行,计算的是第二行的大小 - 16
	//2. *(a + 1) == a[1], a[1]是第二行的数组名,sizeof(*(a + 1))就相当于sizeof(a[1]),意思是把第二行的数组名单独放在
	//sizeof内部,计算的是第二行的大小
	printf("%d\n", sizeof(&a[0] + 1));//a[0]是第一行的数组名,&a[0]取出的就是数组的地址,就是第一行的地址
	//&a[0]+1 就是第二行的地址,是地址大小就是4/8个字节
	printf("%d\n", sizeof(*(&a[0] + 1)));//*(&a[0] + 1)意思是对第二行的地址解引用,访问的就是第二行,大小是16字节
	printf("%d\n", sizeof(*a));//a作为数组名并没有单独放在sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,也就是
	//第一行的地址,*a就是第一行,计算的就是第一行的大小,16字节
	//*a == *(a+0) == a[0]
	printf("%d\n", sizeof(a[3]));//a[3]无需真实存在,仅仅通过类型的推断就能算出长度
	//a[3]是第四行的数组名,单独放在sizeof内部,计算的是第四行的大小,16个字节

	//sizeof(int);//4
	//sizeof(3 + 5);//4

	return 0;
}

        读起来有点绕,建议多看几遍。

        关于sizeof的特点再提几句:

        1)里面可以放类型,直接计算类型的大小

        2)sizeof里面的内容不会真的拿去运算,只会判断类型。

        3)sizeof里面的数组越界了也没关系,因为不会真的访问它。

五. 作者感想

        指针是个蛮复杂的东西,想要学好需要打下牢固的基础。        

        除此之外,写博客好累,而且这才写第一遍。感觉写的不太好,之后回头再润润色。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值