梦开始的地方 —— C语言指针进阶


指针进阶

0.前言

从上一篇博客指针入门我们知道了,几点

  1. 指针是一个变量,存放的是地址,地址是某一块内存空间的唯一标识
  2. 指针的大小在不同平台的大小不一样,32位平台的是4个字节,64位平台是8个字节
  3. 指针的类型决定了指针解引用和指针加减能走多远

1. const修饰指针

我们知道const修饰一个变量,它就是一个常变量是不会被直接修改的。

#include <stdio.h>

int main()
{
	const int num = 10;
	num = 10; //这里会报错
	return 0;
}

当我们可以通过指针来进行修改,也就是不直接对它进行修改,而是取出它的地址通过地址来对它进行修改。

#include <stdio.h>

int main()
{
	const int num = 10;
	int* p = &num;
	*p = 20; //num被修改成了20
	printf("%d\n", num);

	return 0;
}

那么const的就失去了原本的意义,其实还有一种另外的方式可以达到我们想要的效果。那就是const修饰指针

代码示例:如果再尝试使用地址的方式修改变量编译器就会报错。

#include <stdio.h>

int main()
{
	int num = 10;
	const int* p = &num;
    //int const* p = &num; 这两个写法没区别
	*p = 20;
	printf("%d\n", num);

	return 0;
}

那么问题又来了,我们不能通过地址修改但此时又可以通过直接修改的方式进行修改

#include <stdio.h>

int main()
{
	int num = 10;
	const int* p = &num;
	num = 20;// num被修改成了20
	printf("%d\n", num);

	return 0;
}

依旧是可以解决的,我们在用一个const修饰一下 p就好了,再用const修饰变量,此时无论是直接修改还是通过取地址都无法修改num的值了,那const到底怎么修饰指针呢?

#include <stdio.h>

int main()
{
	const int num = 10;//让num不能通过变量名修改
	const int* const p = &num;//不允许通过*修改num的地址,同时指针变量p也不能被改变
	printf("%d\n", num);

	return 0;
}

总结:

  • const写在星号*左边,修饰的是指针指向的内容,使得指针指向的内容,不能通过指针来改变。但是指针变量本身是可以修改的。

    #include <stdio.h>
    
    int main()
    {
    	int num = 10;
    	int n = 100;
    	const int*  p = &num;
    	*p = 20;//错误写法
    	p = &n; //正确写法
    	num = 200;//正确写法
    
    	return 0;
    }
    
  • const写在星号*右边,修饰的是指针变量本身,使得指针变量本身不能修改。但是指针指向的内容可以通过指针来改变,是可以修改的。

    #include <stdio.h>
    
    int main()
    {
    	int num = 10;
    	int n = 100;
    	int* const  p = &num;
    	*p = 20;//正确写法
    	p = &n; //错误写法
    	num = 200; // 正确写法
    	return 0;
    }
    

2. 字符指针

在C语言中字符指针用char*来表示

#include <stdio.h>

int main()
{
    char c = 'Q';
    char* cp = &c;
    printf("%c\n", *p);

    return 0;
}

char*还有另外一种用途,那就是常量字符串

#include <stdio.h>

int main()
{
    char* str = "hello";// 常量字符串
    char arr[] = "hello";
    printf("%c\n", *str);
    printf("%s\n", str);
    printf("%s\n", arr);

    return 0;
}

str 里存放的是“hello”字符串的首地址,且这是一个常量字符串,也就是它不能被修改

而arr是一个字符数组它是可以被修改的

在这里插入图片描述

再来看一段代码

#include <stdio.h>

int main()
{
    char* str1 = "hello";
    char* str2 = "hello";

    char arr1[] = "hello";
    char arr2[] = "hello";
    
    if (str1 == str2)
    {
        printf("str1 == str2\n");
    }
    else
    {
        printf("str1 != str2\n");
    }
    if (arr1 == arr2)
    {
        printf("arr1 == arr2\n");
    }
    else
    {
        printf("arr1 != arr2\n");
    }
    
    return 0;
}

运行结果

str1 == str2
arr1 != arr2

str1 == str2 是因为,"hello"是一个常量字符串,常量字符串是不能被修改的。所以常量字符在内存中只会存一份,所以 str1和str2存放的都是 “hello”这个字符的首地址

而arr1 和 arr2 是数组名,数组名存储放的是首元素的地址,这是两个不同的数字当然不相等。

注意:比较字符串相等要使用 strcmp

在这里插入图片描述

2. 指针数组

整形数组、字符数组都是数组,那么指针数组也是数组,它是一个存放指针的数组。上一篇博客也提到过

基本语法

#include <stdio.h>

int main()
{
    int* arr[10];//整形指针数组
    char* ch[10]; //字符指针数组
    int** arrP[10]; //二级整形指针数组

    int a = 10;
    char c = 'a';
    int* p = &a;

    arr[0] = &a;
    ch[0] = &c;
    arrP[0] = &p;
    
    printf("%d\n",*arr[0]);
    printf("%c\n",*ch[0]);
    printf("%d\n",**arrP[0]);//第一次解引用拿到一级指针p的地址,再次解引用就拿到了a的地址
    
    return 0;
}

3. 数组指针

1) 数组指针概念

数组指针是指针还是指针?

我们知道整形指针int *指向的是一个整形变量,也知道字符指针char*指向的是一个字符变量,那么数组指针它也是一个指针,它是一个指向数组的指针。

那么下面两个哪个才是数组指针呢?

int *arrP[10];
int (*pArr)[10];
  • int *arrP[10],因为[]的优先级要高于*,所以arrP会和[]先结合变成arrP[10]这就是一个数组,那么int就会和*结合变成int *,那么此时就变成了一个int*类型的数组,也就是整形指针数组
  • (*pArr)括起来,那么它们结合就是一个指针,指向了一个数组[10],所以这里可以描述为,*pArr指向了一个10个元素的数组,每个元素的类型是int,所以int (*pArr)[10]是一个数组指针

2) 数组名 和 &数组名

我们给一个数组

int arr[10]

这个数组的arr&arr分别是啥?

来看一段代码

#include <stdio.h>

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


    return 0;
}

打印结果:发现arr和&arr的地址是一样的?我们知道arr存的是数组首元素的地址,那 &arr也是吗?并不是

00000030B82FFCF8
00000030B82FFCF8

再来看一段代码

#include <stdio.h>

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

    return 0;
}

打印结果

000000FA108FF608
000000FA108FF608
==========
000000FA108FF60C
000000FA108FF630

我们发现,arr&arr打印的地址是一样的。但是它们的意义是完全不一样的。

arr+1只是跳过了4个字节的地址,而&arr+1则是条过了40个字节的地址,相当于跳过了整个数组。

从这里看出来&arr取出的是整个数组的地址!

注意:

  • &数组名
  • sizeof(数组名)
  • 除此上面2种情况之外-所有遇到的数组名都是数组首元素的地址

3) 数组指针的使用

了解了数组指针,那么数组指针是怎么使用的呢?

整形指针存的是整形的地址,那么数组指针指向的是一个数组,那它存的就是数组的地址。

前面我们了解到通过指针±可以遍历数组,指针p存的是数组首元素的地址,那么怎么通过数组指针遍历数组呢?

#include <stdio.h>

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

数组指针的使用,通过使用数组指针,用三种不同的方式遍历整个数组(注意:一下写法一般不会使用)

#include <stdio.h>

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int(*pArr)[10] = &arr;
    int i = 0;
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", (*pArr)[i]);
    }
    printf("\n");
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", *(*pArr+i));
    }
    printf("\n");
    for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf("%d ", pArr[0][i]);
        //等价于
        //printf("%d ", *(*(p) + i))
    }

    return 0;
}
  • (*pArr)拿到的是整个数组的地址,而整个数组的地址是有arr来维护的,其实*pArr[0]等价于arr[0],也就可以理解为 *pArr <==> arr
  • 第二种写法:*pArr等价于arr,那么其实它就是首元素的地址,那么*pArr+i其实拿到的是下标为 i 元素的地址,再对其解引用拿到的就是这个元素
  • 第三种写法:pArr[0]其实等价于*(pArr+0),也就是说 pArr[0][i]<==> *(*(p+0) + i))

上面的那些写法让人决得很变扭,一般不会这么写。

来看看正常的数组指针的使用,一般是用于二维数组

我们通过函数来打印数组正常传参,传过去的是二维数组就通过二维数组来接收。其实还可以通过数组指针来接收。

#include <stdio.h>

void prt1(int arr[3][4], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }

}
void prt2(int (*p)[4], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int  j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%d ", *(*(p + i) + j));
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
    prt1(arr, 3, 4);
    prt2(arr, 3, 4);

    return 0;
}

数组名是首元素地址,而二维数组的数组名则是二维数组第一行的地址。

那么*(*(p+i)+j)是什么意思呢?

  • p 是第一行的地址
  • p+i 是跳过i行后的那一行的地址
  • *(p+i) 跳过i行之后的那一行,也就相当于这一行的数组名
  • (*(p+i)+j) 跳过i行后的那一行的下标为j的元素地址
  • *(*(p+i)+j) 跳过i行后的那一行的小标为j的元素

需要注意的是,这里传递过去的是数组数组名,也就是数组首元素的地址,也就是二维数组的第一行。

再来看一段代码

#include <stdio.h>

int main()
{
	int arr[3][4] = { 0 };
	int (*p1)[4] = arr;//二维数组首元素,也就是第一行的地址


	int (*p2)[3][4] = &arr;//这是整个二维数组的地址
	printf("%p\n", p1);
	printf("%p\n", p2);
	printf("==============\n");
	printf("%p\n", p1+1);
	printf("%p\n", p2+1);
	return 0;
}

运行结果:我们发现p1+1只是跳过了一行一行4个元素也就是16个字节,而p2+1跳过了整个二维数组,也就是48个字节。说明二维数组其实和一维数组,数组名和&数组名是一样的。

0000002D8135F4B8
0000002D8135F4B8
==============
0000002D8135F4C8
0000002D8135F4E8

4) 简单汇总

//这是普通数组
int arr[10];
// pArr1和[]结合这是一个数组,int*结合,所以这一个 指针数组
int *pArr1[10];
// *和pArr2结合,这是一个指针,指向的是一个存放10个元素的数组,数组的类型是Int。所以这是一个指针数组
int (*pArr2)[10]; 
// pArr3和[]先结合这是一个数组,它是存放数组指针的数组
int (*pArr3[10])[5]; 

解释一下最后一个数组

pArr3和[]结合他是一个数组,每个数组有10个元素

把数组名去掉,剩下的就是元素类型 int (*)[5],这是一个指针指向一个元素5个元素的数组,每个数组的元素类型是int

比如 int (*p)[5] = &arr; 这是一个数组指针,假设把p去掉 int(*)[5],剩下的就是它的元素类型了。

pArr3[10]是一个存放数组指针的数组,数组有10个元素

所以这是一个

int main()
{
    int arr1[5] = { 0 };
    int arr2[5] = { 0 };
    int arr3[5] = { 0 };
    int (*pArr3[10])[5] = { &arr1,&arr2,&arr3 };


    return 0;
}

在这里插入图片描述

4. 数组和指针传参

1) 一维数组传参

#include <stdio.h>
//数组传参数组接收没问题
void test1(int arr[]){}
//数组传参数组接收写了元素类型也没问题(这本质是一个指针,写不写都一样)
void test1(int arr[10]){}
//传数组名,本质上就是首元素的地址,没问题
void test1(int* arr){}
//传指针数组,指针数组接收每问题
void test2(int* arr[20]){}
//指针数组的数组名本质上也是一个地址,数组的每个元素都是int* 类型,那它的首元素就是一个一级指针,用一个二级指针接收一级指针每有任何问题
void test2(int** arr){}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test1(arr);
	test2(arr2);
}

2) 二维数组传参

#include <stdio.h>
//传二维数组用二维数组接收每任何问题
void test(int arr[3][5]){}
//在C语言中二维数组的列是不能省略的!
void test(int arr[][]){}
//省略行每任何问题,省略行没问题,可以不知道有多少行,但必须知道每行有多少列,猜方便运算
void test(int arr[][5]){}
//二维数组传的是二维数组首元素也就是第一行的地址,拿一个int* 的指针来接收肯定是不行的。可以存放到数组指针去
void test(int* arr){}
//这是明显错误的,这么写表示这是个整形指针数组
void test(int* arr[5]){}
//这是正确的写法,这是一个指针指向了一个二维数组第一行的地址,数组每一个行有5个元素,
void test(int(*arr)[5]){}
//错误写法,这是一个二级指针,只能用来接收一个一级指针的地址
void test(int** arr){}
int main()
{
	int arr[3][5] = { 0 };
	test(arr)}

3) 一级指针传参

一个int*的指针能接收什么参数?

#include <stdio.h>
void test(int *p)
{

}

int main()
{
	int a = 10;
	int *pa = &a;
	int arr[10] = {0};
    //下面三中写法都是正确的
	test(&a);
	test(pa);
	test(arr);

	return 0;
}

char*又能接受什么参数?

char*有两种表示形式,一种是字符指针,一种是字符串,存的是字符串的收地址。

#include <stdio.h>
void test(char *p)
{

}

int main()
{
	char c = 'a';
	char str[] = "hello";
	char arr[] = {'h','e','l','l','o','\0'};
	test(&c);
	test(str);
	test(arr);

	return 0;
}

4) 二级指针传参

二级指针存放的是一级指针的地址,所以传递个一级指针的地址,或者二级指针都是可以的

#include <stdio.h>
void test(int** p)
{
	printf("%d\n", **p);
}

int main()
{
	int a = 10;
	int* p = &a;
	int** pp = &p;
	test(&p);
	test(pp);

	return 0;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱敲代码的三毛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值