资讯专栏INFORMATION COLUMN

C语言进阶:指针的进阶

浠ラ箍 / 2043人阅读

摘要:本章节在此基础上,对语言阶段指针进行更深层次的研究。数组指针的类型由数组类型决定,先找出数组的类型去掉名就是类型。相当于数组指针所指向数组的数组名。数组指针指向整个数组,将其看作二维数组并解引用得到一行的首元素,从而遍历访问。

指针进阶

我们在初阶时就已经接触过指针,了解了指针的相关内容,有:

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

本章节在此基础上,对C语言阶段指针进行更深层次的研究。

字符指针

字符指针,存入字符的地址,类型为char *

字符指针的作用
  1. 指向单个字符变量
char ch = "w";const char* pch = &ch;

这种很容易理解,就是指针解引用访问字符变量。

  1. 指向字符串首字符
char* pc = "hello";printf("%s/n", pc);

这种是把字符串"hello"放进指针嘛?

其实不然,类似于数组名,该指针存的是常量字符串"hello"的首字符的地址。通过对指针解引用访问首字符地址,从而找到整个字符串。

char* pc = "hello";printf("%c/n", *(pc + 1));//eprintf("%s/n", pc);//helloprintf("%s/n", pc + 1);//ello

字符串本质上还是在空间上连续存放,所以指针±整数同样有访问的效果。由此也可以看出%s的用法,把地址给%s会将其后的内容看作字符串并打印直到/0 。(所以我猜测%s的s是string的意思)

字符指针的特点

例题

char str1[] = "hello bit";char str2[] = "hello bit";char* str3 = "hello bit";char* str4 = "hello bit";if (str1 == str2)    printf("str1 = str2/n");//1else    printf("str1 != str2/n");//2if (str3 == str4)    printf("str3 = str4/n");//3else    printf("str3 != str4/n");//4

str1(3)==str2(4),比较的是二者其实位置地址是否相同。(地址才是真正判断二者是否相同的要素)

答案是2和3。因为1和2是用字符串初始化数组,3和4是指针指向常量字符串。

  • str1和str2是普通的数组,是在内存上开辟了两块空间不过存放了一样的数据。
  • str3和str4指向常量字符串,存放在内存的常量区,是不可被修改且具有唯一性即常量区只存放一个。所以str3和str4指向的都是同一个字符串。

常量区的存储特点:存放在常量区的数据不可被修改,正因为不可修改所以存一份就够了。后期如果需要,使用的是同一数据。(数据还是同一个数据,只是用不同的指针维护)

总结

  1. 常量字符串不可被修改,存放在内存的常量区。
  2. 具有唯一性即常量区只存放一个。
指针数组
指针数组的定义
int arr[10];//整型数组char ch[5];//字符数组float f[20];//浮点型数组

可见,元素类型也就是数组的“类型”。

char* pch[5];int* parr[10];float* pf[20];

指针数组就是存放指针的数组。

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

整型数组的数组名arr,即首元素地址,是一级指针。

指针数组的数组名parr,也是首元素地址,不过其首元素为int*类型变量,所以parr就是二级指针。

指针数组的使用
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");}//答案1 2 3 4 52 3 4 5 63 4 5 6 7ps:parr[i] <==> *(parr+i) *(parr[i]+j) <==> *(*(parr+i)+j) <==> (*parr+i)[j] <==> parr[i][j]

通过指针数组访问整型数组的每个元素。parr[i][j]*(*(parr+i)+j)本质上是等价的。

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

打印字符串使用%s更简单,若要使用%c,就是得到每个字符串的起始地址,分别向后访问。

从这里也可以看出数组和指针的关系,我愿称之为*[]的爱恨情仇!

 

数组指针

由前面的例子,不难得出,数组指针是指向数组的指针,是指针而非数组。

数组指针的定义
char ch = "w";char* pch = &ch;//字符地址存放在字符指针中int a = 10;int* pint = &a;//整型地址存放在整型指针中float f = 0.0;float* pf = &f;//浮点型地址存放在浮点型指针中

什么变量的地址存放在什么指针中。指针指向变量的类型,决定了指针的类型。顾名思义,数组指针指向的是数组。

递推可得,数组的地址存放在数组指针中。且数组指针的类型为数组的类型再加个*

下面那种定义方法是对的呢?

int arr[10] = { 0 };//1.int* pa = arr;//2.&arr;//整个数组的地址int* parr = &arr;//3.int* parr[10] = &arr;//4.int(*parr)[10] = &arr;
  1. 取出的是首元素的地址,而非整个数组的地址
  2. 整型指针应存放整型变量的地址,数组的地址无法存入整型指针中。
  3. []的优先级比*高,故parr先与[]结合成数组名,所以parr是个指针数组。

数组指针的类型由数组类型决定,先找出数组的类型int[10](去掉名就是类型)。且不能让[]先与parr结合,所以用()先将parr*结合,即成int(*parr)[10]

C语言规定[]必须再最后面,所以不可写成int[10](*parr)

int* parr[10];//指针数组int(*parr)[10];//数组指针

我们前面强调过,去掉名字就是类型。所以int[10]是整型数组的类型,int*[10]是指针数组的类型,int(*)[10]是数组指针的类型。

&数组名和数组名

之前介绍过不止一遍,所以这次只说重点。

指针类型决定了指针±整数的步长。

//首元素地址+1printf("%p/n", arr);//0073FCA4printf("%p/n", arr + 1);//0073FCA8//整个数组地址+1printf("%p/n", &arr);//0073FCA4printf("%p/n", &arr + 1);//0073FCCC
  1. 首元素地址就是整型指针+1,自然只能向后访问4shou个字节
  2. 整个数组地址+1,即int(*)[10]型指针+1,向后访问了 i n t × 10 int×10 int×10即40个字节。

sizeof(arr)也代表整个数组,现在去理解为什么sizeof里数组名代表的是整个数组呢?数组这种结构保存了数组的大小,sizeof求所占空间的长度,那自然要严谨一些了。

数组指针的使用

遍历数组,使用数组或是指针作形参接收就行了。且所谓的用数组接收仅是理解层面,本质上都是指针。

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;}
反面用例

数组作实参,用数组或指针接收即可。数组指针使用对了很好用,但如果随便用可能会很别扭。下面先介绍强行使用数组指针的用法。

//错误示范void Print3(int(*pa)[10], int sz) {	for (int i = 0; i < sz; i++) {		//printf("%d ", pa[i]);		printf("%d ", *(pa + i));	}}

将整个数组地址传过去,则用数组指针接收,然后呢,直接对pa解引用吗?

结果显然是错误的,从结果中也可以看出打印的是十进制下的地址,+1跳过40个字节。

这里笔者在学习的时候产生了个疑问,传过去数组的地址,为什么解一层引用后还是地址呢?

&arr解引用*后相当于找到首元素的地址,可以理解为&*相互抵消只剩下arr不就是首元素的地址嘛~

void Print4(int(*pa)[10], int sz) {	for (int i = 0; i < sz; i++) {		printf("%d ", *(*(pa)+j));	}}

倘若我们把一维数组看作是二维数组第一行。由于二维数组在内存中是连续存放的,我们只打印二维数组的第一行,便可以避免上面的错误。


style=“zoom:80%;” />

*(pa)相当于数组指针所指向数组的数组名。数组指针指向整个数组,将其看作二维数组并解引用得到一行的首元素,从而遍历访问。

正面用例

从上面的例子也可以看出,用数组指针访问二维数组时,效果便不错。

//二维数组传参,用二维数组接收void Print1(int arr[3][5], int r, int c) {	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 };	Print2(arr, 3, 5);//二维数组首元素是首行    	return 0;}
  • 把二维数组想象成一个拥有三个元素的一维数组(每个元素也为一维数组),即一维数组的一维数组。
  • 由于其每个元素是有5个元素的一维数组,数组指针定义为int(*p)[5],指向首行这个“一维数组”。(传参穿的是数组名)
  • 第一层循环用于“跳行”,即每次跳过5个元素。第二层循环遍历每行“一维数组”。

  1. 用二维数组和数组指针接收的都是首行地址。
  2. 数组指针的类型int(*)[5],和二维数组首元素地址的类型相同。

故可得,二维数组首元素地址和数组指针是等价的,即数组指针pa就是数组名

二维数组首元素为其首行,相当于一个一维数组,该一维数组的地址类型为int(*)[5]。且实参为二维数组名,降级为指向首行的指针,所以它是数组指针,类型为int(*)[5]

数组指针指向二维数组,才是使用数组指针的正确示范。

Example

下列示例分别是什么?

//1.int arr[5];//2.int *pa1[5];//3.int (*pa2)[10];//4.int (*pa3[10])[5];
  1. 整型数组
  2. 存放整型指针的数组

*靠左靠右无所谓,pa1先和[]结合为数组,剩下int*为数组元素类型。

  1. 指向整型数组的指针

(*pa2)*先和pa2结合为指针,剩下int[10],指向的是元素个数为10的整型数组。

  1. 存放数组指针的数组

pa3先和[10]结合为数组,剩下int(*)[5]是指向数组的指针为数组的元素。所以是个元素个数为10的数组指针数组。

逆向思考,有整型数组arr[5]和指向该数组的类型为int(*)[5]的数组指针,还有数组指针数组pa3[10]用于存放该数组指针。

类型辨别方法
  1. 若名称先和[]结合为数组,只去掉数组名就是数组类型,去掉[n]和数组名便是其元素的类型。
  2. 若名称先和*结合为指针,只去掉指针名就是指针类型,去掉*和指针名便是指向的变量的类型。

 

数组传参和指针传参

实践之中不免会碰到数组和指针作函数参数而如何设计形参的问题。

一维数组传参

一维数组传参,下列接收方式是否可行呢?

//1.void test(int arr[]) {}//2.void test(int arr[10]) {}//3.void test(int* arr) {}int main(){		int arr[10] = { 0 };	test(arr);	return 0;}
  1. 数组传参数组接收,可行但其实都会降级优化成指针,编译器不会真正创建一个数组。

  2. 由于形参数组形同虚设,所以数组大小无意义,任意大小或无。(有歧义)

  3. 数组传参本质就是首元素地址,首元素类型为int,所以指针的类型为int*

所以可以看出[]*()是等价的。我愿称之为*[]的爱恨情仇!(‐^▽^‐)

//1.void test2(int* arr[2]){}//2.void test2(int** arr) {}int main(){		int* arr2[10] = { 0 };	test2(arr2);	return 0;}

指针数组,每个元素类型为int*,故用二级指针接收数组名。

一维数组传参,数组和指针接收。

二维数组传参
//1.void test(int arr[3][5]) {}//2.void test(int arr[][]){}//3.void test(int arr[][5]){}int main() {	int arr[3][5] = { 0 };	test(arr)
            
                     
             
               

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/118796.html

相关文章

  • C语言进阶指针进阶

    摘要:故使用无具体类型,又称通用类型,即可以接收任意类型的指针,但是无法进行指针运算解引用,整数等。求指针所占字节而不是解引用访问权限大小。数组就是整个数组的大小,数组元素则是数组元素的大小,指针大小都为。 ...

    ingood 评论0 收藏0
  • 玩转指针,手撕c语言——(指针进阶

    摘要:函数的返回值为指针就按照字面意思,指针函数的定义顾名思义,指针函数即返回指针的函数。 目录 前言指针与函数函数的返回值为指针作为函数参数的指针指针函数可以改变变量...

    genedna 评论0 收藏0
  • C语言进阶:动态内存管理

    摘要:释放不完全导致内存泄漏。既然把柔性数组放在动态内存管理一章,可见二者有必然的联系。包含柔性数组的结构用进行动态内存分配,且分配的内存应大于结构大小,以满足柔性数组的预期。使用含柔性数组的结构体,需配合以等动态内存分配函数。 ...

    shinezejian 评论0 收藏0
  • 三文读透指针语法【中篇】@指针进阶---函数指针+函数指针数组+指向函数指针数组指针

    摘要:三文读透指针上篇本文将继续介绍有关函数指针的相关内容。在大型工程里,函数指针应用还是挺普遍的。首先看阅读下面两段有趣的代码出自语言陷阱与缺陷看看他们是什么意思代码代码函数指针数组函数指针数组,即存放函数指针的数组。 ...

    blastz 评论0 收藏0

发表评论

0条评论

浠ラ箍

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<