详解C语言指针我真的让C指针给我唱征服了~乌拉

Posted 大猩猩!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解C语言指针我真的让C指针给我唱征服了~乌拉相关的知识,希望对你有一定的参考价值。

文章目录


前言

众所周知,C指针算是C最难的一部分。在这我会将和大家一起深究指针并征服C指针。

在正式学习进阶指针之前,我们先来简单回忆下指针基础

指针定义:指针变量,用于存放地址。地址唯一对应一块内存空间。
指针大小:32位平台下占4个字节,64位平台占8个字节。
指针类型:类型决定指针±整数的步长和指针解引用时访问的大小。
指针运算:指针解引用,指针±整数,指针-指针,指针的关系运算。
指针诞生:

这些内容在C基础部分已经讲过,铁汁们可以复习一下👉传送门👈,看完记得回来啊~

理解一遍后,就让我们正式起航扬帆吧!乌拉~
注: 我们习惯把指针变量叫作指针,本文指针本质是指针变量


一、字符指针

1.字符指针的定义

字符指针: 指向字符的指针,类型为char*


2.字符指针的作用

  1. 指向单个字符变量
	char ch = "w";
	char* pch = &ch;
  1. 指向字符串首字符
	char* pc = "hello";
	printf("%s\\n",pc);

看图说话:
(为了理解简单,地址用201、204表示)

①比较好理解,pch存有ch的地址,因此可通过解引用操作访问ch
②并不是把字符串"hello"放进指针,而是把字符串首字符的地址放进指针,通过首字符地址,可以找到整个字符串

对于上面的解析,我们可以进行验证:

为什么靠一个首字符的地址就可以找到整个字符串呢?

下面我们来证明一下:

	char* pc = "hello";
	printf("%c\\n", *(pc + 1));//e
	printf("%c\\n", *(pc + 2));//l
	printf("%s\\n", pc);//hello
	printf("%s\\n", pc + 1);//ello

运行结果:

  1. 字符串中每1个字符占1个字节,且内存的单元是1个字节,为了方便管理,字符串在内存空间上是连续存放的
    即hello是连续的
  2. %s:输出字符串,从所给地址开始,一直打印到\\0结束符号(不包括'\\0')

3.字符指针的特点

这是一道题面试题:

#include<stdio.h>
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");
	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),表示比较存放相同字符串的两个指针的值是否相同

我们可以做出分析:

1.str1[]和str2[]是字符数组,在内存上会开辟两块地址不一样空间,但存放相同的内容"hello bit"
2.str3和str4是指向常量字符串的指针,而常量字符串存放在内存的常量区,常量区特点是常量值不可被修改有唯一性(就没有存在2份或多份的必要),所以指针指向的是同一份数据,故地址是相同的
图解:

注: 常量区的常量不可修改,通常用const来修饰。防止被意外修改


二、指针数组

1.指针数组的定义

我们先看:

int arr[10]; 整型数组
char ch[5];  字符数组
float f[20]; 浮点型数组

整型数组是存放整型的数组。
类比得指针数组是存放指针的数组

int* parr[10];整型指针数组
char* pch[5];字符指针数组
float* pf[20];浮点型指针数组

关于指针数组的数组名:

int arr[10];
int* parr[10];

数组的数组名是首元素的地址:
整型数组的数组名arr,是首元素(整型)的地址,所以arr是一级指针。
整型指针数组的数组名parr,也是首元素(整型指针)的地址,所以parr是二级指针。


2.指针数组的使用

整型指针数组的使用:

#include<stdio.h>
int main()

    //int a = 10;
    //int b = 20;
    //int c = 30;
    //int* arr[3] =  &a, &b, &c ;//不常用的写法
    
    int arr1[] =  1,2,3,4,5 ;
    int arr2[] =  2,3,4,5,6 ;
    int arr3[] =  3,4,5,6,7 ;
    int* parr[] =  arr1,arr2,arr3 ;//常见的写法
    for (int i = 0; i < 3; i++) 
    
        for (int j = 0; j < 5; j++) 
        
            //1.
            printf("%d ", parr[i][j]);
            //2.
         // printf("%d ", *(*(parr + i) + j));
        
        printf("\\n");
    
    return 0;

运行结果:

通过指针数组访问整型数组的每一个元素
parr[i][j]等价于*(*(parr + i) + j)

字符指针数组的使用:

#include<stdio.h>
int main()

    const char* pch[] =  "abcde", "bcdef", "cdefg" ;
    for (int i = 0; i < 3; i++)
    
        //1.
        printf("%s", pch[i]);
        //2.
    //  printf("%s", *(pch + i));
        printf("\\n");
         

运行结果:

pch[i]是字符串首字符的地址,%s:打印字符串


三、数组指针

1.数组指针的定义

我们已经知道:
字符指针:指向字符的指针
整型指针:指向整型的指针

C语言语法是有规律性的,也就是说,指针指向的类型<==>指针的类型(相互决定)
所以我们可以类比得:
数组指针:指向数组的指针,即数组的地址存放在数组指针中。

到此,我要说一下,指针部分会出现一些复杂的类型,那如何理解一个复杂的类型?

在这我先给大家抛出一个屡试不爽的技巧总结,有了这些法宝,后面的路就会容易走。

类型显得复杂的原因是它由多种的运算符组成,当我们分清运算符的优先级,理清顺序,自然会柳暗花明又一村~

1.运算符主要有三种,它们的优先级是:* < [ ] < ( )
2.变量名第一次与运算符结合就决定了它的本质。(变量名的处女情结?_?😂)
如,先与*结合是指针;先于[ ]结合是数组;
( ):1.先于( )结合是函数,即p( ) 2.用于改变优先级,如(*p)

下面我们一起来探索一下吧!

int* p[10];
int(*p)[10];
int p(int);
int (*p)(int);
int *(*p(int))[10];

int* p[10];//由优先级知:p先于[]结合,则p是数组;后与*结合,则是指针数组;最后与int结合,则是整型指针数组
int(*p)[10];//()优先级最高,p先与*结合,则p是指针;后[]结合,则是数组指针;最后与int结合,则是整型数组指针
int p(int);//p先与( )结合,则p是函数;参数是整型,返回值是整型,则是参数为整型,返回值是整型的函数
int (*p)(int);//p先与*结合,则p是指针;后与( )结合,则指针指向的是函数,函数参数是整型,返回值是整型,则是一个指向参数为整型,返回值是整型的函数的指针(函数指针)
int *(*p(int))[10];//p先与( )结合,则p是形参为int的函数;后与*结合,则是返回指针的函数;再与[ ]结合,则是返回的指针指向的是一个数组;再与*结合,说明数组里的元素是指针;最后与int结合,指针指向的内容是整型数据。所以p是返回值为整型指针数组指针,形参为int型的函数。


2.细说指针

好了,现在我们已经学会了如何理解一个复杂的类型。那对于一个指针,我们要搞清楚它的什么呢?

指针是一个特殊的变量,它存放着内存中的地址。
要深入了解它从这四方面考虑:

2.1.指针类型

上一章说过,去掉名字就是类型
同理,若把指针声明语句中的指针的名字去掉,剩下部分就是该指针的类型。

int* ptr;//指针的类型是int*
char* ptr;//指针的类型是char*
int** ptr;//指针的类型是int**
int (*ptr)[5];//指针的类型是int()[5]
int* (*ptr)[10];//指针的类型是int
(*)[10]

指针类型的意义(C基础篇已经讲过):

1.指针解引用访问几个字节(访问的内存空间大小)
2.指针类型决定了指针±整数跳过几个字节(步长)

2.2.指针所指向的类型

指针所指向的类型决定了编译器看待指针指向内存区的内容的方式
若把指针声明语句中的指针的名字名字左边的指针声明符号*去掉,剩下部分就是指针所指向的类型。

int* ptr;//指针所指向的类型是int
char* ptr;//指针所指向的类型是char
int** ptr;//指针所指向的类型是是int*
int (*ptr)[5];//指针所指向的类型是int()[5]
int* (*ptr)[10];//指针所指向的类型是int()[10]

通过观察我们可以发现:

二者可互推

2.3.指针的值

指针的值:指针里存放的地址
举个例子:

int *p;	//定义一个指针
int a;	//定义一个int类型的变量
p=&a;	//使用取址运算符(&)将变量a的地址赋给p

指针的值:p本身的值,p里存放这变量a的内存的起始地址
而指针p所指向的内存区就是从a的起始地址开始,长度为size(int)的一片内存区。

2.4.指针大小

指针大小:32位平台下占4个字节,64位平台占8个字节。


3.数组名相关

老生常谈:

①sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
②&数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
除这两种外,数组名都是数组首元素的地址。

#include<stdio.h>
int main()

	int arr[10] = 0;
	int* p1 = arr;//arr是数组首元素地址,为int型
	int (*p2)[10] = &arr;//&arr是整个数组的地址,为int [10]型
	//arr和&arr值一样,但类型不一样
	
	//p1和p2是相同的指向同一位置
	printf("%p\\n", p1);//204
	printf("%p\\n", p2);//204
	//指针类型决定指针±整数的步长
	printf("%p\\n", p1 + 1);//跳过一个整型,208
	printf("%p\\n", p2 + 1);//跳过一个数组,244
	return 0;
//为了简单理解,  204,208,244表示地址


4.数组指针的使用

当我们遍历一维数组时,可以这样做:

void Print1(int arr[], int sz)

	for (int i = 0; i < sz; i++)
	
		//printf("%d ", arr[i]); 
		printf("%d ", *(arr + i));
	

void Print2(int* arr, int sz)

	for (int i = 0; i < sz; i++)
	
		printf("%d ", arr[i]);
		//printf("%d ", *(arr + i));
	

int main()

	int arr[10] =  1,2,3,4,5,6,7,8,9,10 ;
	int sz = sizeof(arr) / sizeof(arr[0]);
	Print1(arr, sz);
	Print2(arr, sz);
	return 0;

运行结果:

把数组名传给函数时,我们有两种接收方式:
①数组接收,编译器会将数组退化为指针
②指针接收
因为数组名是数组首元素的地址,用指针接收是正确的。

当数组指针用来接收并访问一维数组时:

#include<stdio.h>
int main()

	int arr[10] = 1,2,3,4,5,6,7,8,9,10;
	int (*parr)[10] = &arr;//指针指向数组,数组有10个元素,每个元素为int型
	int i = 0;
	for(i = 0; i < 10; i++)
	
		printf("%d ", *((*parr) + i));//*parr相当于arr
	
	return 0;

通常这种写法显得小题大作,比较别扭,非常不推荐这种写法。

通常,数组指针用来接收并访问二维数组,会有很好的效果:

void Print1(int arr[3][5], int r, int c)//二维数组传参,用二维数组接收,实际上不会创建二维数组,编译器会将int arr[3][5]退化为int(*pa)[5]

	for (int i = 0; i < r; i++)
	
		for (int j = 0; j < c; j++)
		
			//printf("%d ", arr[i][j]);
			printf("%d ", *(*(arr + i) + j));
		
		printf("\\n");
	

void Print2(int(*pa)[5], int r, int c)//二维数组传参,用数组指针接收

	for (int i = 0; i < r; i++)
	
		for (int j = 0; j < c; j++)
		
            //1.
            printf("%d ", pa[i][j]);
            //2.
		//	printf("%d ", *(*(pa + i) + j));
		
		printf("\\n");
	

int main()

	int arr[3][5] =  1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 ;
	Print1(arr, 3, 5);//二维数组首元素是首行 
	Print2(arr, 3, 5);//二维数组首元素是首行 
	return 0;

运行结果:

分析:
在C基础数组章节,我们知道二维数组在内存中也是连续存储的。
在这里简单提下,需要复习的,👉C基础数组传送门 👈
连续存储:1.每一行内部的元素连续存放 2.行与行之间连续存放

  • 因此,二维数组的数组名是首行的地址,类型是int(*)[5]
  • 二维数组首元素地址和数组指针是等价的,即数组指针pa就是数组名。
  • 指针类型是int(*)[5],解一层引用找到的是二维数组的行
  • 指针所指向的类型是int[5],再解一层引用找到的是某行中的元素

综上,正确的做法是:使用数组指针来接收二维数组

正确使用数组指针会有很好的效果,但如果随便用可能会很别扭。
下面是强行使用数组指针的错误用法:

void Print3(int(*pa)[10], int sz)

	for (int i = 0; i < sz; i++)
	
		//printf("%d ", pa[i]);
		printf("%d ", *(pa + i));
	

int main() 
	int arr[10] =  1,2,3,4,5,6,7,8,9,10 ;//一维数组
	int sz = sizeof(arr) / sizeof(arr[0]);
	Print3(&arr, sz);//&arr是整个数组的地址
	return 0;


实参为整个数组的地址,形参是数组指针,*(pa+i)解一层引用后,打印出来的为什么还是地址呢?

  • &是取地址运算符,*是间接运算符。&*互为逆运算
  • *&x 的含义是,先获取变量 x 的地址,再获取地址中的内容,相当于抵消
  • 所以&arr传给pa,执行*pa,相当于*&arr,实质就是arr,还是整个数组的地址。
  • 整个数组的地址的类型是int(*)[10],即指针pa的类型是int(*)[10]
  • 由指针类型决定指针±整数的步长可知,每个地址间相差40字节。

四、数组传参和指针传参

写代码时要把数组和指针传递给函数的情况在所难免,那函数参数该如何设计呢?

1.一维数组传参

一维数组传参,下面的接收方式它合理吗?

void test(int arr[])//合理吗?

void test(int arr[10])//合理吗?

void test(int *arr)//合理吗?

void test2(int *arr[])//合理吗?

void test2(int *arr[20])//合理吗?

void test2(int **arr)//合理吗?

int main()

    int arr[10] = 0;
    int学习C语言的指针部分有啥好的方法吗?真的很难啊!!

详解C语言那些可怕的野指针

C语言 怎样让一个指针的内容传给一个变量

C++智能指针详解(真的很经典 )

《征服 C 指针》摘录6:解读 C 的声明

搞透C语言指针那年我双手插兜, 不知道指针是我的对手