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

Posted 爱敲代码的三毛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了梦开始的地方 —— 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<

以上是关于梦开始的地方 —— C语言指针进阶的主要内容,如果未能解决你的问题,请参考以下文章

梦开始的地方——C语言指针练习题

梦开始的地方 —— C语言: 函数指针+函数指针数组+指向函数指针数组的指针

梦开始的地方——C语言文件操作详解

梦开始的地方—— C语言预处理+编译过程

梦开始的地方 —— C语言常用字符函数汇总

梦开始的地方——C语言柔性数组