「地表最强」C语言(十一)指针

环境:CLion2021.3;64位macOS Big Sur

文章目录

  • 十一、指针
    • 11.1 指针是什么
    • 11.2 指针和指针类型
    • 11.3 野指针
    • 11.4 指针运算
    • 11.5 指针和数组
    • 11.6 二级指针
    • 11.7 指针数组
    • 11.8 const和指针
    • 11.9 指针的分类及应用
      • 11.9.1 字符指针
      • 11.9.2 指针数组
      • 11.9.3 数组指针
      • 11.9.4 数组传参和指针传参
      • 11.9.5 函数指针
      • 11.9.6 函数指针数组
      • 11.9.7 指向函数指针数组的指针
      • 11.9.8 回调函数
      • 11.9.9 指针和数组面试题的解析

地表最强C语言系列传送门:
「地表最强」C语言(一)基本数据类型
「地表最强」C语言(二)变量和常量
「地表最强」C语言(三)字符串+转义字符+注释
「地表最强」C语言(四)分支语句
「地表最强」C语言(五)循环语句
「地表最强」C语言(六)函数
「地表最强」C语言(七)数组
「地表最强」C语言(八)操作符
「地表最强」C语言(九)关键字
「地表最强」C语言(十)#define定义常量和宏
「地表最强」C语言(十一)指针
「地表最强」C语言(十二)结构体、枚举和联合体
「地表最强」C语言(十三)动态内存管理,含柔性数组
「地表最强」C语言(十四)文件
「地表最强」C语言(十五)程序的环境和预处理
「地表最强」C语言(十六)一些自定义函数
「地表最强」C语言(十七)阅读程序

十一、指针

11.1 指针是什么

指针其实就是地址。首先了解内存地址是怎么编号的,以32位机器为例:
32位 – 32根地址线 – 物理线 – 通电 – 1/0
由此完成了电信号到数字信号的转换,得到了1和0组成的二进制序列:

00000000 00000000 00000000 00000000
……
 11111111  11111111   11111111   11111111
共可以表示2^32个地址,也就是这么多个存储单元。
其中一个存储单元占一个字节。

	int a = 10;//a在内存中需分配4个字节。
	printf("a的地址:%p\n", &a);//%p专门用来打印地址。
	int *pa = &a;//a为4个字节,取地址取得为第一个字节的地址,这个地址是低位的。
	// *说明pa是指针变量,int说明pa指向的对象是int类型的。
	*pa = 20;// * 解引用操作,*pa就是通过pa中的地址找到a。
	printf("通过指针修改a的值:%d\n", *pa);
	++pa;
	printf("自增后的地址:%p\n",pa);//自增4个字节

	char ch = 'q';
	char *pc = &ch;
	printf("ch的地址:%p\n", pc);
	printf("ch的下一个地址:%p\n", ++pc);//自增一个字节

	//指针的大小是相同的:指针式用来存放地址的,指针需要多大空间取决于地址的存储需要多大空间
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(short*));
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(long*));
	printf("%d\n", sizeof(long long*));
	printf("%d\n", sizeof(float*));
	printf("%d\n", sizeof(double*));

「地表最强」C语言(十一)指针_第1张图片

11.2 指针和指针类型

指针类型的意义:
1.指针类型决定了指针解引用的权限有多大。

	int a = 0x11223344;
	char *pc = &a;
	*pc = 0;//由于pc为char类型指针,因此此处只能访问一个字节,将其改为0
	int *pa = &a;
	*pa = 0;//由于pa为int类型指针,此处可以访问四个字节,将其改为0

观察内存中的变化:
初始情况
用*pc赋值
用*pa赋值
2.指针类型决定了指针走一步能走多远,即步长。

	int arr[10] = { 0 };
	int* p = arr;
	char* pc = arr;
	printf("%p\n", p);
	printf("%p\n", p + 1);//int型,走4个字节
	printf("%p\n", pc);
	printf("%p\n", pc + 1);//char型,走1个字节

「地表最强」C语言(十一)指针_第2张图片

	int arr[10] = { 0 };
	//int* p = arr;//跳过4个字节,即以元素为单位访问
	char* p = arr;//一个字节一个字节改变
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = 1;
	}

「地表最强」C语言(十一)指针_第3张图片
「地表最强」C语言(十一)指针_第4张图片

11.3 野指针

野指针:指针指向的位置是不可知的(随机的、不正确的,没有明确权限的)
造成野指针的原因:
1.指针未初始化:

	int* p;//p是一个局部变量,局部变量不初始化的话,默认的是随机值
	*p = 20;//非法访问内存了,因为p所指向的空间是随机的,这里的p就是一个野指针

2.指针越界访问:

	int arr[10] = { 0 };
	int* p = arr;
	for (int i = 0; i <= 10; i++)
	{
		*p = i;
		p++;//当i=10的时候,再访问arr中的元素就越界了
	}

3.指针指向的空间释放:

int* test()
{
	int a = 10;
	return &a;//函数调用结束后a的空间就被释放了,如果再操作这块儿空间实际上是非法的。
}
	int* p = test();
	*p = 20;

如何避免野指针:
1.指针要初始化:

	int* p = NULL;//当前不知道p应该初始化为什么地址的时候,直接初始化为NULL
	int a = 10;
	int* p = &a;//明确知道初始化的值

2.小心越界,C语言本身不会检查越界行为;
3.指针指向的空间释放时,要将指针置空;
4.指针使用之前进行有效性检查:

	int* p = NULL;
	if(p != NULL)
		*p = 50;

11.4 指针运算

1.指针 + - 整数:

	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int* pend = arr + 9;//等价于arr[0]
	while (p <= pend)//指针关系运算
	{
		printf("%d\n", *p);
		p++;
	}

2.指针 - 指针,前提是两个指针指向同一块空间,即同一类型:
指针 + 指针无意义

	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d", &arr[9] - &arr[0]);//指针减指针,得到指针之间元素的个数

模拟strlen():

int myStrlen(char* str)
{
	char* start = str;
	while('\0' != str)
	{
		str++;
	} 
	return str - start;
}
size_t myStrlen(const char* str)
{
	assert(str != NULL);
	//assert(str);
	size_t len = 0;
	while (*str++ != '\0')
	{
		len++;
	}
	return len;
}

3.指针的关系运算
标准规定:允许指向数组元素的指针与指向数组最后一个元素和后边的那个内存位置的指针比较
但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for (vp = &values[0]; vp < &values[N_VALUES];)
{
     *vp++ = 0;
}

11.5 指针和数组

	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;//数组名
	//[]是一个操作符	2和arr是两个操作数
	printf("%d\n", 2[arr]);	//arr[2] --> *(arr+2) <==> *(2+arr) --> 2[arr]
	printf("%d\n", arr[2]);
	printf("%d\n", p[2]); //p[2] --> *(p+2) <==> *(2+p) --> 2[p]
	//arr[2] <==> *(arr+2) <==> *(p+2) <==> *(2+p) <==> *(2+arr)
	//2[arr] <==> *(2+arr)
	//[]操作符经过编译后的处理就转换为了解引用操作符*(),在根据其操作数进行调整
	//指针也可以当作数组名使用,非常灵活。

11.6 二级指针

	int a = 10;
	int* pa = &a;//pa是指针变量,一级指针
	int* *ppa = &pa;//pa也是变量,&pa取出pa在内存中的起始地址,ppa就是一个二级指针变量
	int** *pppa = &ppa;

11.7 指针数组

	int arr[10];//整型数组 - 存放整形的数组
	char ch[5];//字符数组 - 存放字符的数组
	//指针数组 - 存放指针的数组
	int* parr[5];//存放整型指针的数组
	char* pch[5];//存放字符指针的数组

11.8 const和指针

	//const修饰变量,这个变量就被称为常变量,不能被修改,但是本质上还是变量。
	const int num = 10;
	int n = 100;
	//num = 20;//err
	int* p = &num;//这样就可以通过p指针更改num的值
	const int* p = &num;//const如果放在*左边,修饰的是*p,表示指针指向的内容不能通过指针来改变,但是指针变量本身是可以修改的
	*p = 20;//err,被const修饰,不可更改p所指向的内容
	p = &n;//*p不能修改,但是p仍可以修改
	const int num = 10;
	int n = 100;
	num = 20;//err
	int* p = &num;//这样就可以更改num的值
	int* const p = &num;//const如果放在*右边,修饰的是指针变量p,表示指针变量不能被改变,但是指针指向的内容可以被修改
	*p = 20;//*p能修改,但是p不可以修改。
	p = &n;//err,p直接被const所修饰,不能更改
	int const* const p = &num;//指针本身和指针指向的变量都不可以改

11.9 指针的分类及应用

11.9.1 字符指针

	char ch = 'q';
	char* pc = &ch;
	char* ps = "hello world";//本质上是把字符串的首字符地址存储在了ps中
	*ps = 'w';//err,常量不能改
	char arr[] = "hello world";
	printf("%c\n", *ps);
	printf("%s\n", ps);
	printf("%s\n", arr);

	char str1[] = "hello world.";
	char str2[] = "hello world.";
	const char* str3 = "hello world.";
	const char* str4 = "hello world.";//"hello world."是常量字符串,在内存中只有一份。两个指针指向同一空间,而不会创建新的空间。
	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");

11.9.2 指针数组

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

	int *arr[3];//存放整型指针的数组
	int a = 10;
	int b = 20;
	int c = 30;
	int* arr[3] = { &a,&b,&c };
	for (int i = 0; i < 3; i++)
		printf("%d ", *(arr[i]));

	int a[5] = { 1,2,3,4,5 };
	int b[] = { 2,3,4,5,6 };
	int c[] = { 3,4,5,6,7 };

	int* arr[3] = { a,b,c };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
			printf("%d ", *(arr[i] + j));
			//printf("%d ",arr[i][j]);
		printf("\n");
	}

11.9.3 数组指针

数组指针是指向数组的指针

	int arr[10] = { 1,2,3,4,5 };
	//arr;//是首元素arr[0]的地址
	int(*parr)[10] = &arr;//parr就是一个数组指针,其中存放的是数组的地址.
	//若无括号,parr先和[]结合,变成了指针数组。
	double* d[5];
	double* (*pd)[5] = &d;//pd就是一个数组指针,指向一个有5个元素的数组,该数组的每个元素都是double*

	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", &arr);//数值一样但是含义不同
	int* p1 = arr;
	int(*p2)[10] = &arr;//取出的是数组的地址
	printf("%p\n", p1);
	printf("%p\n", p1 + 1);//p1指向一个整型,加一会增加一个整型的长度4
	printf("%p\n", p2);
	printf("%p\n", p2 + 1);//p2指向一个数组,加一会增加一个数组的长度40
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*pa)[10] = &arr;//[]优先级高于*,若不加括号,则表示指针数组
	for (int i = 0; i < 10; i++)
		printf("%d ", *(*pa + i));//数组指针用在一维数组很繁琐
		//*pa拿到的是arr首元素的地址,是int型,因此+i会增加一个整型长度
		//若直接pa+i,则增加i个数组的长度

	int arr[5]	//arr是整型数组。
	int *parr1[10]	//parr1是整型指针的数组。
	int (*parr2)[10]		//parr2是数组指针,该指针指向一个数组,数组有10个元素,每个元素是int型。
	int (*parr3[10])[5];		//parr3是一个数组,该数组有10个元素,每个元素是一个数组指针,每个指针指向一个有5个元素的数组,每个元素位int型
	//数组名[]去掉以后,剩下的就是数组每个元素的类型
	void ptint2(int (*p)[5], int row, int col)
	{
		for(int i = 0;i < row;i++)
		{
			for(int j = 0;j < col;j++)
				printf("%d ",*(*(p + i) + j));
			printf("\n");
		}
	}
	
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	print2(arr, 3, 5);

11.9.4 数组传参和指针传参

判断接收参数类型设置的是否符合其实看两点即可:
(1)看传递过来的参数是什么
(2)看这个东西是什么类型的
符合以上两点即可

1.一维数组传参

	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);//ok
	test2(arr2);//ok
	//接收arr
	void test(int arr[]) {}//ok
	void test(int arr[10]) {}//ok
	void test(int* arr) {}//ok
	//接收arr2
	void test(int* arr[20]) {}//ok
	void test(int** arr) {}//ok

2.二维数组传参

	int arr[3][5] = { 0 };
	void test(int arr[3][5]) {}//ok
	void test(int arr[][5]) {}//ok
	void test(int arr[][]) {}//err
	void test(int* arr) {}//err,其首元素地址收一个指针数组而非整型数组
	void test(int* arr[5]) {}//err
	void test(int (*arr)[5]) {}//ok
	void test(int** arr) {}//err
	

3.一级指针传参

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

	char ch = 'w';
	char* p1 = &ch;
	test(&ch);
	test(p1);
	
	return 0;
}

4.二级指针传参

void test2(int** p2)
{
	**p2 = 20;
}

int main()
{
	int a = 10;
	int* pa = &a;//pa是一级指针
	int* *ppa = &pa;//ppa是二级指针
	//传递二级指针
	test2(ppa);//传二级指针
	test2(&pa);//传一级指针变量的地址
	int* arr[10] = { 0 };
	test2(arr);//传一级指针数组
	printf("%d", a);
	return 0;
}

11.9.5 函数指针

函数指针是指向函数的指针,即存放函数地址的指针。
&函数名得到的就是函数的地址。
数组名 != &数组名,函数名 == &函数名

void test3(char* str){}
int main()
{
	void (*pt)(char*) = &test3;
	printf("%p\n", &Add);
	printf("%p\n", Add);
	int (*pf)(int, int) = &Add;//pf就是一个函数指针变量
	int (*pf)(int, int) = Add;//Add <==> pf
	printf("%d\n",(****pf)(3, 5));//对函数指针解引用来调用函数,其实*无意义,写几个都行		1
	printf("%d\n",Add(3, 5));	//		2
	printf("%d\n",pf(3, 5)); //这三行等价			3			1 <==> 2 <==> 3
	return 0;
}

练习见14.3

11.9.6 函数指针数组

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

	int (*pf1)(int, int) = Add;
	int (*pf2)(int, int) = Sub;
	int (*pfArr[2])(int, int) = { pf1,Sub };//pfArr为函数指针数组,pfArr首先与方括号结合,说明是数组
	//去掉pfArr[2]后剩下 int (*)(int,int)说明是函数指针,指向参数为两个int,返回值为int的函数。

设计简易计算器

int main()
{
	int (*pArr[4])(int , int) = { Add, Sub, Mul, Div };
	int input = 0;
	do
	{
		menu();
		scanf("%d",&input);
		int x = 0;
		int y = 0;
		if (0 <= (input - 1) && 3 >= (input - 1))
		{
			scanf("%d %d", &x, &y);
			printf("%d", pArr[input - 1](x, y));
		}
		else if(input == 0)
			printf("即将退出\n");
		else
			printf("请输入有效选项\n");
	}while(input);
	
	return 0;
}

11.9.7 指向函数指针数组的指针

	int(*p)(int, int);//函数指针
	int(*pArr[10])(int, int);//函数指针的数组
	int(*(*pa)[10])(int, int);//pa是指向函数指针数组的指针
	//首先,*pa是指针,将(*pa)拿掉,看出其指向有10个元素的数组,将(*pa)[10]拿掉,看出这个数字
	//的每一个元素都是一个指向函数的指针,这个函数有连个int型的参数,返回值为int型

11.9.8 回调函数

1.回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或者条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

使用回调函数实现计算器:

int Cacl(int (*pfun)(int,int))
{
	int x = 0;
	int y = 0;
	printf("请输入两个操作数:\n")
	scanf("%d %d",&x,&y);
	return pfun(x, y);
}

int main()
{
	int input = 0;
	do
	{
		menu();
		scanf("%d", &input);
		switch(input)
		{
		case 0:
			printf("即将退出\n");
			break;
		case 1:
			printf("%d\n",Cacl(Add));
			break;
		case 2:
			printf("%d\n",Cacl(Sub));
			break;
		case 3:
			printf("%d\n",Cacl(Mul));
			break;
		case 4:
			printf("%d\n",Cacl(Div));
			break;
		default:
			printf("请输入有效选项\n");
			break;
		}
	}while(input);
	
	return 0;
}

2.qsort()函数
void qsort( void *base, size_t num, size_t width, int (*compare )(const void *elem1, const void *elem2 ) );
void* base:待排序数据的首元素的地址。
size_t num:待排序数据中元素的个数和。
size_t width:待排序数据中,每个元素的大小,单位字节。
int (*compare )(const void *elem1, const void *elem2 ):能判断任意类型元素大小的函数,elem1>elem2,返回1;相等返回0;否则返回-1.

模拟qsort()实现任意类型数据的冒泡排序,以结构体型为例:

struct Stu
{
	char name[20];
	int age;
};

int cmp_struct(void* ele1,void* ele2)
{
	//调用bubbleAnyType()时,此函数的地址为第四个参数,需要自己实现
	//先将形参强制类型转换转换为待比较数据的类型的指针,然后再操作
	return strcmp(((struct Stu*)ele1) -> name,((struct Stu*)ele2) -> name);//按名字排序
	//return (struct Stu*)ele1.age- (struct Stu*)ele2.age;//按年龄排序
}

void swap(char* ele1,char* ele2,int width)
{
	//交换两个元素,以字节为单位交换,因此形参设置为char*即可
	for(int i = 0; i < width; i++)
	{
		char tmp = *ele1;
		*ele1 = *ele2;
		*ele2 = tmp;
		ele1++;
		ele2++;
	}
}

void bubbleAnyType(void* base,int size,int width,int (*compare)(const void* e1, const void* e2))
{
	for(int i = 0; i < size - 1; i++)
	{
		for(int j = 0; j < size -1 -i; j++)
		{
		//以字节为单位操作,因为字节是各类型中最小的存储单位
		//首地址+数据类型的大小,就找到了要操作的数据
			if( compare( (char*)base + j * width, (char*)base + (j + 1) * width) > 0 )
			{//交换元素
				swap( (char*)base + j * width, (char*)base + (j + 1) * width, width) );
			}
		}	
	}
}

int main()
{
	struct Stu s[3] = { {"zhangsan",30},{"lisi",35},{"wangwu",25} };
	int sz = sizeof(s) / sizeof(s[0]);
	bubbleAnyType(s,sz,sizeof(s[0]),cmp_struct);
	return 0;
}

排序前:
在这里插入图片描述
排序后(以按姓名排序为例):
在这里插入图片描述

11.9.9 指针和数组面试题的解析

sizeof(数组名)数组名单独放在括号内 和 &数组名 分别是结算整个数组的大小 和 取出整个数组的地址。除此之外,所有数组名都是首元素的地址。

	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));			//	16
	printf("%d\n", sizeof(a + 0));		//	4	a+0是第一个元素的地址
	printf("%d\n", sizeof(*a));			//	4	a是1的地址,*a表示1
	printf("%d\n", sizeof(a + 1));		//	4	a+1是第二个元素的地址
	printf("%d\n", sizeof(a[1]));		//	4	
	printf("%d\n", sizeof(&a));			//	4
	printf("%d\n", sizeof(*&a));		//	16	*&其实相互抵消了
	printf("%d\n", sizeof(&a + 1));		//	4	数组后边的空间的地址,都是地址	*(&a + 1)能访问一个数组的大小
	printf("%d\n", sizeof(&a[0]));		//	4	无论指向哪里,都是地址
	printf("%d\n", sizeof(&a[0] + 1));	//	4	无论指向哪里,都是地址
	//字符数组			strlen接收的参数是字符指针
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", sizeof(arr));			// 6
	printf("%d\n", sizeof(arr + 0));		// 4
	printf("%d\n", sizeof(*arr));			// 1
	printf("%d\n", sizeof(arr[1]));			// 1
	printf("%d\n", sizeof(&arr));			// 4
	printf("%d\n", sizeof(&arr + 1));		// 4
	printf("%d\n", sizeof(&arr[0] + 1));	// 4
	printf("%d\n", strlen(arr));			// 随机
	printf("%d\n", strlen(arr + 0));		// 随机
	printf("%d\n", strlen(*arr));			// err	将97当成地址处理,发生了错误
	printf("%d\n", strlen(arr[1]));			// err	将98当成地址处理,发生了错误
	printf("%d\n", strlen(&arr));			// 随机,整个字符数组的地址和首元素地址相同,char(*)[6] -> char *
	printf("%d\n", strlen(&arr + 1));		// 随机 - 6		跳过了一个数组的大小
	printf("%d\n", strlen(&arr[0] + 1));	// 随机 - 1		跳过了一个字符的大小
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));			// 7		双引号默认有个'\0'
	printf("%d\n", sizeof(arr + 0));		// 4		为首元素地址
	printf("%d\n", sizeof(*arr));			// 1		a字符的大小
	printf("%d\n", sizeof(arr[1]));			// 1		b字符的大小
	printf("%d\n", sizeof(&arr));			// 4		char(*)[7]
	printf("%d\n", sizeof(&arr + 1));		// 4		是个地址
	printf("%d\n", sizeof(&arr[0] + 1));	// 4		是个地址 
	printf("%d\n", strlen(arr));			// 6		字符串长度不算'\0'
	printf("%d\n", strlen(arr + 0));		// 6		
	printf("%d\n", strlen(*arr));			// err		没有意义,strlen接受的实际上是一个指针
	printf("%d\n", strlen(arr[1]));			// err
	printf("%d\n", strlen(&arr));			// 6		默认有'\0'
	printf("%d\n", strlen(&arr + 1));		// 随机		跳过一个数组,无法确定下一个'\0'的位置
	printf("%d\n", strlen(&arr[0] + 1));	// 5
	char* p = "abcdef";//	p指向首地址
	printf("%d\n", sizeof(p));				// 4
	printf("%d\n", sizeof(p + 1));			// 4
	printf("%d\n", sizeof(*p));				// 1
	printf("%d\n", sizeof(p[0]));			// 1		p[0] <=> *(p + 0)
	printf("%d\n", sizeof(&p));				// 4
	printf("%d\n", sizeof(&p + 1));			// 4
	printf("%d\n", sizeof(&p[0] + 1));		// 4
	printf("%d\n", strlen(p));			// 6
	printf("%d\n", strlen(p + 1));		// 5
	printf("%d\n", strlen(*p));			// err *p是a,传递的是a的ASCLL码值,出错
	printf("%d\n", strlen(p[0]));		// err 传递的是a的ASCLL码值,出错
	printf("%d\n", strlen(&p));			// 随机1
	printf("%d\n", strlen(&p + 1));		// 随机2
	printf("%d\n", strlen(&p[0] + 1));	// 5	+1加多少,看取出来的是什么类型

解释一下倒数第2和3个,如图,由于不知道&p的内容是什么,无法确定’\0’的位置,因此随机值:
「地表最强」C语言(十一)指针_第5张图片

二维数组的首元素是第一行

	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));				// 48
	printf("%d\n", sizeof(a[0][0]));		// 4
	printf("%d\n", sizeof(a[0]));			// 16	a[0]可以理解为第一行数组名,计算的是第一行数组的大小,因为sizeof括号内只有数组名
	printf("%d\n", sizeof(a[0] + 1));		// 4	数组名a[0]非单独在括号内,也没有&,表示数组首元素地址,即第一行第二个元素的地址,+1跳过一个整型即a[0] + 1 表示a[0][1]
	printf("%d\n", sizeof(*(a[0] + 1)));	// 4	*(a[0] + 1))是第一行第二个元素
	printf("%d\n", sizeof(a + 1));			// 4	a是二维数组名,但没有&或者单独在sizeof()内,因此a为首元素地址,二维数组的首元素是第一行,+1跳过一行,为第二行的地址
	printf("%d\n", sizeof(*(a + 1)));		// 16	(a + 1)为第二行的地址,解引用找到的是整个第二行		*(a + 1) <=> a[1]
	printf("%d\n", sizeof(&a[0] + 1));		// 4	整个第一行的地址 + 1 = 整个第二行的地址
	printf("%d\n", sizeof(*(&a[0] + 1)));	// 16	
	printf("%d\n", sizeof(*a));				// 16	二位数组的第一个元素为第一行的一维数组		*a <=> *(a + 0) <=> a[0]
	printf("%d\n", sizeof(a[3]));			// 16	表达式的两个属性:值属性和类型属性	a[3]的类型是int [4]
											//这里虽然没有访问a[3],但是可以知道它的类型,也就知道了它的大小。a[3] <=> *(a + 3)
	//sizeof(表达式):表达式并不会计算,所以不会发生越界的情况,但是表达式结果的类型是知道的。

说明一下最后一行,如图,黑色框为内存中数组a[3][4]所占用的空间(实际上是连续存放在一行的,即这12个元素,地址都是连续的,为了便于展示,写成这个形式)。
接下来思考一个问题:
a[0][1]代表第一行第二列的元素,a[1][2]代表第二行第三列的元素,a[2][3]代表第三行第四列的元素,然后我们暂且忽略列号,只看上述的三个行号,即a[0],a[1],a[2],分别来看一下:
a[0][1],若把a[0]看为一个名字,后边的[1]就好像在访问名字为a[0]的数组的第二个元素一样;
a[1][2],若把a[1]看为一个名字,后边的[2]就好像在访问名字为a[1]的数组的第三个元素一样;
a[2][3],若把a[2]看为一个名字,后边的[3]就好像在访问名字为a[2]的数组的第三个元素一样;
事实也是如此,二维数组的数组名[行号]完全可以作为当前行的一维数组的数组名。而数组名又表示了首元素的地址,注意,他是一个地址,a[0]是地址,a[1]和a[2]是地址,a[3]当然也是一个地址,因此自然存在这个东西。
那a[3]在当前代码下是否越界呢?很明显,这个地址范围超过了我们定义的数组的方位,因此如果要访问这块儿空间,那一定是非法访问。但是代码此处为sizeof(a[3]),这就不得不介绍一下sizeof了。
sizeof是一个编译时的操作符,即在编译期间就会将数据处理好,在程序运行时不会去执行sizeof内部的表达式,因此a[3]是不被执行的,因此当然不会越界;那不运行怎么会知道sizeof的值是多少呢?
实际上,每个表达式都有两个属性:1.值属性;2.类型属性。
值属性当然需要通过计算,但是类型属性完全不用计算,计算机就可以知道它是什么类型。如本题中的a[3],经过上边的分析我们知道了,他其实是一个数组名,其类型时int [4],我们有知道,数组名在两种情况下表示整个数组:1.sizeof(数组名),即sizeof括号内单独出现时,计算的是整个数组的大小;
2.&数组名,取到的是整个数组的地址。而此处恰恰是第一种情况,所以他计算的是整个第四行的大小,即4(int) * 4(bytes) = 16 bytes
「地表最强」C语言(十一)指针_第6张图片
阅读程序题见:地表最强C语言汇总(十四)阅读程序(持续更新)第4-11题

你可能感兴趣的:(C语言,c语言,c++,开发语言)