C语言篇+ 指针进阶(上)

Posted IT莫扎特

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言篇+ 指针进阶(上)相关的知识,希望对你有一定的参考价值。


一、前言

在上一篇博客中作者已将指针的基础知识归纳在这篇博客中,《 指针和结构体(初级)》,而今天我们要进入指针的下一个环节,指针进阶!!为了怕大家忘记之前讲过的知识,本人再次梳理出一些知识,请各位耐心观看,以下又是老调重弹

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算。

1.1进入主题

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<assert.h>
#include <stdio.h>

int main() 
{
	char *p = NULL;
	p = "hello word";
	return 0;
}

进入正题,我们都知道指针的大小(所占用的字节),是跟操作系统有关的,指针的大小是固定的4/8个字节(32位平台/64位平台),而字符在内存中是占用一个字节的,那么上面的代码是将整个的 hello word 存放到字符指针中去,还是只存放字符串的第一个字符呢?

在内存窗口中我们可以看到指针变量p存放的恰好就是字符串的首字符的地址,直到后面的 00 00 ,前面的都是有效字符,试想字符在内存中是占一个字节的,如果要把整个字符串存放到一个指针变量中去,那不就得溢出了吗,所以存放的是首字符的地址,字符串在内存中是挨着存的,有了首字符的地址不难找到其他字符串内容,另外值得一提的一点就是字符串是存放在字符串常量区的,既然是一个常量,那么就不可以通过别的方式对其修改,否则就会引发程序出错,如果不小心修改了怎么办呢,建议在前面加const,这样即使你想改,编译器也会编译不过去

1.2牛刀小试

你见过这样的一道题吗?

#include <stdio.h>
int main()
{
    char str1[] = "hello word.";
    char str2[] = "hello word.";
    const char *str3 = "hello word.";
    const char *str4 = "hello word.";
    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; 
}

请把你心目中的答案,在心里默默的讲一句
没错我想你应该知道了,但是这里还是得再说一遍

第一个if语句中比较的是两个地址,因为数组名是字符首地址,那么既然是不同的数组名,想必在内存中的存放位置也是不一样的,即使在内存中存放的值都是 hello word ,但是在比较的时候还是比较的地址,试想不是在同一块内存开辟的同一块空间,他们的地址会一样吗?显然不是,而在 str3 和 str4比较的时候比较的也是地址,并且这两个指针变量指向的都是一块空间,既然是指向同一块空间,那么指向的就是字符首地址,因为在内存中压根就不会存放两份一样的数据,我们都知道字符串是常量不能被修改,既然是不可变的那么被两个指针去指向这一份不会被修改的内容又有什么关系呢而且还不会浪费内存,所以在这个地方比较的是同一个地址,当我们的程序执行起来,可以看到和我们的预期结果是一样的

结论:
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始
化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同

二、指针数组

指针数组的主体是一个数组,数组中存放的每一个元素是一个指针

int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组

这么理解指针数组的语法:

就拿arr1来说,[ ]的优先级是最高的,所以[ ]会先和arr1结合,那么arr1表示的是一个数组,而数组的元素类型是什么,int * 表示的就是数组的元素类型,所以arr1是一个每个元素都是int *的指针数组

1.1指针数组的初始化

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;
	
	int *arr[4] = {&a,&b,&c,&d};
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d ", *(arr[i]));
	}
	return 0;
}

数组是连续存储的,而指针数组的每一个元素都是一个指针变量,并且这个指针变量指向的元素类型是int类型,所以在初始化的时候是将变量的地址存入到数组中,以下就是指针数组在内存中的布局,而打印的过程也很简单,通过数组下标索引数组中的每一个元素,元素对应变量的地址,对地址解引用就能找到此地址对应的变量值

1.2指针数组存放一维数组的地址

int main() 
{
	int arr1[5] = { 1,2,3,4,5 };
	int arr2[5] = { 2,3,4,5,6 };
	int arr3[5] = { 3,4,5,6,7 };

	int *parr[3] = { arr1,arr2,arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++) 
		{
			printf("%d ",parr[i][j]);
			//另外一种写法  printf("%d ",*(*(parr + i) + j));
		}
		printf("\\n");
	}
	
	return 0;
}

parr是一个指针数组,这个指针数组存放的元素是一维数组的地址,每一个元素类型是int 因为数组名是数组首元素的地址(数组首地址),而数组首地址的类型是int ,所以指针数组的每一个元素都是int ,而指针数组的类型是int [3]
printf("%d ",
(
(parr + i) + j)); 如何理解这句代码,
(parr + i)是找到下标为i的那个元素,而这个元素是一个数组首地址啊
(parr + i) + j,找到了一维数组的首地址再偏移j个长度,不是就找到了一维数组中的第j个元素的地址了吗,((parr + i) + j),再通过()解引用就能找到地址上对应的数值,其实(*(parr + i) + j) 和 parr[i][j]这两种访问数组元素的原理是差不多的

int main() 
{
	char *arr[] = { "春眠不觉晓","处处闻啼鸟","夜来风雨声","花落知多少" };
	
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%s\\n",arr[i]);
	}
	return 0;
}

字符指针数组的内存布局

指针数组中存放的每一个元素都是一个指针变量,这个指针变量的类型char*,每一个元素都指向着字符串起始地址,在以%s打印的时候会通过指针指向的起始地址找到‘\\0’出现的前面字符,依次打印

三、数组指针

数组指针的主体是指针,该指针指向一个数组首地址(数组名)

int (*parr)[10]

这么理解数组指针的语法,()的优先级是最高的,所以* 会先和
parr结合,这么一看是一个指针,往后一看[10],表示的是指针指向的数组的元素个数是10,前面的int 表示数组指针指向的数组的每一个元素都是int ,而数组指针的类型是 int (*)[10]

1.1&数组名VS数组名

前提概念:1、数组名是数组首地址,数组名表示数组首元素的地址
2、&arr取出的是整个数组的地址
3、&arr的类型是数组指针 int (*)[10]

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


从打印结果可以看出数组首元素的地址 + 1会跳过一个整形,而&数组名会跳过40个整形,原因是因为&arr的类型是 int(*)[10],对它 + 1会跳过40个字节,这里的地址不是数组首元素的地址,而是数组的地址,在这里要有一个概念性的了解

1.2数组指针的使用

1.2.1数组指针的错误示范

void func(int (*arr)[10],int sz) 
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ",arr[i]);//err
		printf("%d ", *(arr + i));//err
	}
}

int main() 
{
	int arr[10] = {1,2,3,4,5,67,8,9,10};
	int len = sizeof(arr) / sizeof(arr[0]);
	func(&arr,len);
	return 0;
}

根据前面的逻辑我们知道&arr取出的是数组的地址,int (*arr)[10] 是个数组指针,本质上还是一个指针,访问的是整个数组的地址,而数组的地址 + 1会偏移40个字节,随后访问的都是随机值。

1.2.2数组指针的正确示范示范

void func(int (*arr)[10],int sz) 
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
	//以下几种方式均可以
		printf("%d ", arr[0][i]);
		printf("%d ",(*arr)[i]);
		printf("%d ",*(*arr + i));
	}
}

int main() 
{
	int arr[10] = {1,2,3,4,5,67,8,9,10};
	int len = sizeof(arr) / sizeof(arr[0]);
	func(&arr,len);
	return 0;
}

解读:

arr [0][i] 有了第一个元素的下标向后访问i下标的元素,因为数组是连续存储的,有了第一个元素的下标,自然也能找到其他下标位置处的元素

(*arr)[i],数组指针是指向数组的地址,对指针解引用就能找到指针指向的内容,*arr表示的是数组的首元素地址,有了首元素的地址就可以向后偏移i位,其实等价于arr[i]

*(*arr + i),数组指针是指向数组的地址,对指针解引用就能找到指针指向的内容,*arr表示的是数组的首元素地址,对首元素的地址(数组首地址),偏移i个长度,会跳过4 * i个字节,再将偏移后的指针解引用找到指针指向的内容

希望以上的解释对大家的理解有所帮助,这也是博主对这些知识在一定程度上的理解,对这些语法的解释

接下来再看看另外一种用法


/* 常规写法 */
void func(int arr[3][5],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 func(int (*arr)[5],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("%d ", *((*arr + 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};
	int row = sizeof(arr) / sizeof(arr[0]);
	int col = sizeof(arr[0]) / sizeof(arr[0][0]);
	func(&arr,row,col);
	return 0;
}

主要还是说一下数组指针的使用方式

首先我们来看这是35的二维数组

当使用数组指针的时候获取的是它的下标为【0】的一维数组的地址,数组指针里面存放的就是它的地址

假设现在要找处arr[1][4]的元素,下面我们直接看图

在这里是通过让数组指针指向下标【1】的位置处,有了数组的起始地址向后偏移,方便找到其他的元素,不过这种int(
)[5]类型的数组指针只是针对一维数组的,存放的是一维数组的地址,这一点请大家务必注意

有了上面知识的理解,再来看一组,你现在知道它们代表着什么吗?

1、int arr[5]; --》 表示的是一个数组,数组的每一个元素都是一个int
2、int *parr1[10]; --》表示的是指针数组,数组中的每个元素都是int *类型的指针变量
3、 int (*parr2)[10]; --》数组指针,本质上是一个指针,指向的是一个数组,数组的元素个数是10,数组的元素类型是int
4、int (*parr3[10])[5]; 重点来了,先看操作符的优先级,()的优先级是最高的,所以先看()里的内容,其次是[ ]优先级是第二,arr3先和[ ] 结合就是一个数组,数组里面存放着10个元素,每一个元素的类型是 int( * )[5],所以每一个元素都是一个数组指针,该数组指针指向的数组是【5】个元素,每一个元素是int类型,

四、数组参数、指针参数

1.1一维数组传参

#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int *arr)//ok
{}
void test2(int *arr[20])//ok
{}
void test2(int **arr)//ok
{}
int main()
{
 int arr[10] = {0};
 int *arr2[20] = {0};
 test(arr);
 test2(arr2);
}

1.2二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//err
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//err
{}
void test(int* arr[5])//err
{}
void test(int (*arr)[5])//ok
{}
void test(int **arr)//err
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

1.3一级指针传参

#include <stdio.h>
void print(int *p, int sz) {
 int i = 0;
 for(i=0; i<sz; i++)
 {
 printf("%d\\n", *(p+i));
 }
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print

以上是关于C语言篇+ 指针进阶(上)的主要内容,如果未能解决你的问题,请参考以下文章

C语言进阶学习笔记二指针的进阶(练习篇)

C语言进阶学习笔记二指针的进阶(重点必看+代码演示+练习)

C语言笔记进阶篇第一章:指针进阶

C语言笔记进阶篇第一章:指针进阶

C语言指针就应该这么学 - 指针的进阶篇

C语言指针全归纳-进阶版