C语言(超详细 万字讲解 逐行解析,帮你零基础玩转c语言)| 深入理解指针 —— 附带笔试题

指针

引言

当谈及C语言中最强大、最具有挑战性的概念时,指针通常会排在前列。指针是一种特殊的变量类型,它存储了内存地址而不是直接存储值。在C语言中,指针为程序员提供了对内存直接访问和操作的能力,这使得C语言在系统编程、嵌入式开发以及性能关键型应用中非常受欢迎。

指针的使用能够实现动态内存分配、数据结构的实现以及函数参数的传递,同时也是许多高级数据结构和算法的基础。然而,指针的灵活性也带来了一些挑战,如悬挂指针、野指针等问题,这些问题可能导致程序运行时的不确定行为甚至是严重的安全漏洞。

本篇文章将深入探讨C语言中指针的概念、各种用法以及常见问题,并提供实例和技巧来帮助大家更好地理解和利用指针。通过学习和掌握指针,你将能够编写更高效、更灵活的C语言程序,并且能够更好地理解计算机内存管理的工作原理。

1.初始指针

在part1我将为大家简单的介绍一下指针的概念以及基本用法和常见问题。


1.1 指针变量

在讲解指针变量之前,先给大家简单举个例子引入一下内存和地址的概念。
例子
假设有一个宿舍楼(学校用来安置学生的),楼里有100个房间,每个房间都有自己的门牌号,来方便我们快速地找到自己的房间。计算机也是的同理,CPU处理数据的时候,需要的数据的存储和读取都是在内存中(计算机用来存放数据的)完成的。那么内存的空间是如何高效的管理呢?
其实,内存中划分为一个个的内存单元,每个内存单元的大小取1个字节(8个bit位)。其中,每个内存单元,相当于一个学生宿舍,一个字节空间里面能放8个bit位,就好比8人间的宿舍,每个人就是一个比特位
生活中,我们把门牌号也叫做地址,在计算机中我们把内存单元的编号也称为地址。地址在内存中都是连续存放的。 ——这个是非常重要的,也是贯穿始终的。 C语言中给地址起的新名字叫做:指针

现在我们已经对内存、指针、地址有一个形象的了解了。接下来我们要学习两个操作符方便以后对 指针变量(用来存放地址的变量) 的使用。

取地址操作符和解引用操作符

1.取地址操作符 &
我们可以通过取地址操作符 &取出变量的内存起始地址,然后把地址存放到一个变量当中,这个变量就是指针变量。
示例

#include<stdio.h>
int main()
{
	int a = 10;  //在内存中开辟一块空间
	int* p = &a; //这里我们使用&操作符取出了变量a的地址 
	return 0;
}


a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个指针变量。
这里的p左边写的是int **是在说明p是指针变量,而前面的int是在说明p指向的是整形类型的对象。

在这里插入图片描述
此图片的意思是:a中存放的是数字10,他的地址是0x0012ff40,int * p = &a;这条语句的作用是,取出a的地址放入到指针变量p中存储。所以p变量中存放的是0x0012ff40。因此也就证明指针变量是指向a的。

2.解引用操作符*
我们可以通过解引用操作符*对指针变量进行解引用,换言之就是 *操作符通过指针变量中存放的地址,找到其指向的空间。
示例

#include<stdio.h>
int main()
{
	int a = 100; //在内存中开辟一块空间
	int* p = &a; //这里我们使用&操作符取出了变量a的地址
	*p = 0;      
	return 0;
}

*p = 0; 

这行代码的意思是,通过对p指针变量的解引用,找到p指向的空间——这个空间对应的就是a的空间。因此我们就可以理解为*p其实就是a变量了;所以*p = 0; ,就是把a改成0;

指针变量的大小

要想知道指针变量的大小,我们就得先明白其大小的由来。

简单的来讲,CPU和内存之间有大量的数据交换,因此我们需要一组线来链接——地址总线。我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。

根据前边所讲,我们可以推断出,32位机器有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。如果指针变量是用来存放地址的,那么指针变量的大小就得是4个字节的空间才可以。同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。

总结
这时我们可能有个疑问,指针指向的内容不同会不会导致指针变量的大小不同呢?
答案是:不会!
指针变量的大小与类型无关,只要是指针类型的变量,其大小只取决于是32位机器(4个字节)还是64位机器(8个字节)。


1.2 指针类型的意义

大家有没有思考过一个问题,指针变量就是用来存放地址的,为什么还要有类型呢?希望我接下来的讲解,能让你心里有一个答案。

指针解引用的权限有多大

示例
在这里插入图片描述
首先int a = 0x11223344;申请了一段内存空间来存放a变量,并赋给a变量一个十六进制的值0x11223344,然后char * pc = &a;通过取地址操作符将a的地址取出放入到了char类型的指针pc中,最后*pc = 0;对指针变量pc进行解引用,找到pc指向的a空间,并给a赋值为0;
重点在于,右面的内存监视窗口我们会发现整形变量a一共四个字节,而11223344只有44变成了00。这就说明char类型(大小为1字节)的指针变量解引用后只改变了int类型(大小为4个字节)的一个字节。——由此我们可以得出指针的类型决定了指针解引用的时候有多大的权限(一次能操作几个字节)。

指针的步长有多大

示例
在这里插入图片描述
这段代码中,将整形数组的地址赋给了两个指针变量,我们可以看到int *类型的指针变量+1跳过了4个字节,char *类型的指针变量+1跳过1个字节。由此可以得出 指针的类型决定了指针向前或者向后走一步有多大(距离)。

void*指针

在指针类型中有⼀种特殊的类型是void*类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。 但是也有局限性, void* 类型的指针不能直接进行指针的±整数和解引用的运算。
示例

#include<stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	char* pc = &a;
	return 0;
}

在这里插入图片描述
在上面的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警告,是因为类型不兼容。而是用void*类型就不会有这样的问题。
使用 void*类型的指针接收地址

int main()
{
   int a = 10;
   void* pa = &a;
   void* pc = &a;
   *pa = 10;
   *pc = 0;
   return 0;
}

在这里插入图片描述
这里我们可以看到,void*类型的指针可以接受不同类型的地址,但是无法直接进行指针运算。

总结
⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据。


1.3 const修饰指针——使其成为常变量不可被修改

const修饰变量可以使其成为常变量不可被修改。
在指针变量中,const摆放的位置不同,对应的作用也不同。

int main()
{
	int a = 10;
	int b = 20;
	int const* p = &a;
	//p = &b;     成功
	//*p = 100;   错误
	return 0;
}

const修饰指针变量时,放在*的左边,限制的是指针指向的内容(*p),不能通过指针来修改指向的内容。但是可以修改指针变量本身的值(修改的是指针变量的指向)。

int main()
{
	int a = 10;
	int b = 20;
	int* const p = &a;
	//p = &b;     错误
	//*p = 100;   成功
	return 0;
}

const修饰指针变量时,放在*的右边,限制的是指针变量本身(p),指针变量不能再指向其他变量了。但是可以通过其他指针变量,修改指针变量指向的内容。


1.4 指针的运算

指针的基本运算有三种:指针 + - 整数 、指针 - 指针 、 指针的关系运算

指针 ± 整数

因为数组在内存中是连续存放的,只要知道第一个元素的地址,就可以通过指针+整数找到后面的所有元素。

#include <stdio.h>
 //指针+- 整数
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; //首先完全初始化了一个数组arr,并在其中存放了1-10,10个数。
	int* p = &arr[0];					    //其次将数组的首元素的地址通过取地址操作符给到了指针变量p。
	int i = 0;
													
	int sz = sizeof(arr) / sizeof(arr[0]);	//代码的下方有讲解
	
	for (i = 0; i < sz; i++)				//这里通过一个for循环
	{										//再加上一个指针+整数的运算实现打印数组中的所有元素。
		printf("%d ", *(p + i));		    //p+i 这⾥就是指针+整数,作用就是通过+整数访问到后边的数组元素
	}
	return 0;
}

其中的int sz = sizeof(arr) / sizeof(arr[0]); 这行代码其中:sizeof计算的是类型的大小,sizeof(arr) 得到的是整个数组的大小(这个比较特殊在后边的数组名的理解会讲到),sizeof(arr[0])得到的是数组中第一个元素的大小,这样两者相除就可以得到数组的个数啦。最后再把得到的个数赋给整型变量sz。

指针 - 指针

前提:两个指针指向同一块空间

指针 - 指针得到的是两个指针之间的元素个数

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d\n",&arr[9] - &arr[0]);		  //因为是指针相减所以我们要将数组元素的地址取出来
	return 0;
}
指针的关系运算
//指针的关系运算
#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	while (p < arr + sz) //指针的⼤小比较     
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}
while (p < arr + sz) //指针的⼤小比较     
	{
		printf("%d ", *p);
		p++;
	}

arr是首元素的地址加上sz就成为了数组最后一个元素的地址,而p指向的是数组arr中首元素的地址,两者的地址相比较,因为数组的首元素位于内存中的较低地址,而最后一个元素位于内存中的较高地址。所以两者地址比较利用while循环和指针+整数的运算实现了打印数组的功能。


1.5 野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

野指针的成因

1.指针未初始化

#include <stdio.h>
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}

2.指针越界访问

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i <= 11; i++)
	{
		//当i=11时,指针指向的范围就超出了数组arr的范围,此时p就是野指针
		*(p++) = i;
	}
	return 0;
}

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

#include <stdio.h>
int* test()
{
	int n = 100;				//进入到函数内部,将100赋给了n,并且将n的地址返回给了指针变量p
	return &n;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

程序看似没有问题,实际上,当跳出函数test()的那一刻,局部变量n的生命周期就已经结束了(程序已经将n的空间释放了),此时将地址返回给指针变量p的时候,指针变量p已经查无此人了。那么指针变量p也就成为野指针了。

如何规避野指针

1.指针初始化
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULLNULL 是C语言中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。

#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL;
 return 0;
}

2.小心指针越界访问
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

3. 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL。

int main()
{
 	int arr[10] = {1,2,3,4,5,67,7,8,9,10};
	int *p = &arr[0];
 	for(i=0; i<10; i++)
 	{
 		*(p++) = i;
 	}
 	//此时p已经越界了,可以把p置为NULL
 	p = NULL;
 	//下次使⽤的时候,判断p不为NULL的时候再使⽤
	//...
 	p = &arr[0];//重新让p获得地址
 	if(p != NULL) //判断
 	{
 		//...
 	}
 	return 0;
}

4.避免返回局部变量的地址
在C语言中,返回局部变量的地址是一个很危险的行为,因为当函数执行完毕并返回后,该局部变量所在的内存空间就会被释放,因此返回的地址将指向一个不再有效的内存位置。这可能会导致内存泄露。
如果确实需要返回一个局部变量的值,可以考虑通过将其值复制到静态变量、全局变量或者动态分配的内存中,并返回这些对象的地址来实现。


2.指针与数组

在part2我将向大家介绍一些指针与数组之间重要的知识点


2.1 数组名的理解

重点
通常情况: 数组名就是数组首元素(第一个元素)的地址。
两个例外:
1.sizeof(数组名),sizeof中单独存放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。

2.&数组名,这里的数组名表示的是整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)

接下来是两个例外情况的示例

#include <stdio.h>
int main()
{
 int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 printf("%d\n", sizeof(arr));
 return 0;
}

输出的结果是:40,如果arr是数组首元素的地址,那输出应该的应该是4/8才对。

#include <stdio.h>
int main()
{
 	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 	printf("&arr[0]    =%p\n", &arr[0]);
	printf("&arr[0]+1  =%p\n", &arr[0]+1);
 	printf("arr        =%p\n", arr);
 	printf("arr+1      =%p\n", arr+1);
 	printf("&arr       =%p\n", &arr);
 	printf("&arr+1     =%p\n", &arr+1);
 	return 0;
}

结果为

&arr[0]    =0077F820
&arr[0]+1  =0077F824
arr        =0077F820
arr+1      =0077F824
&arr       =0077F820
&arr+1     =0077F848

这里我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是首元素的地址,+1就是跳过⼀个元素。
但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。


2.2 使用指针访问数组

在这里插入图片描述
数组名arr是数组首元素的地址,可以直接赋值给p,其实数组名arr和p在这里是等价的。那我们可以使用arr[i]访问数组的元素,同时p[i]也可以访问数组,将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 是等价于 *(p+i)。同理arr[i] 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的。


2.3 一维数组传参的本质

首先从⼀个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给⼀个函数后,在函数内部求数组的元素个数吗?

#include <stdio.h>
void test(int arr[])
{
 	int sz2 = sizeof(arr)/sizeof(arr[0]);
 	printf("sz2 = %d\n", sz2);
}
int main()
{
 	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 	int sz1 = sizeof(arr)/sizeof(arr[0]);
 	printf("sz1 = %d\n", sz1);
 	test(arr);
 	return 0;
}

结果为

sz1 = 10
sz2 = 1 

我们发现在函数内部是没有正确获得数组的元素个数。因为在test()函数的实参中存放的是arr意为首元素的地址,那么test()函数的形参中理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写sizeof(arr) 计算的是⼀个首元素的地址的大小(单位字节)而不是整个数组的大小(单位字节)。正是因为test()函数的形参数部分本质上是指针,所以在函数内部是没办法求得数组元素个数的。

void test(int arr[])//参数写成数组形式,本质上还是指针
{
 	printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
 	printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
int main()
{
 	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 	test(arr);
 	return 0;
}

一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

总结
1.数组传参的本质就是传递数组首元素的地址,所以形参访问的数组和实参的数组是同一个数组。

2.形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略掉数组的大小的。


2.4 二级指针

指针变量也是变量,是变量就会有地址,那么存放指针变量地址的职责就交给了二级指针。
在这里插入图片描述
希望这一张图就可以让大家理解二级指针。


2.5 指针数组模拟二维指针

首先我们要了解一下指针数组的概念
在这里插入图片描述
指针数组的每个元素是地址,地址又可以指向一块区域。

下面是模拟二维数组的代码

#include <stdio.h>
int main()
{
 	int arr1[] = {1,2,3,4,5};
 	int arr2[] = {2,3,4,5,6};
 	int arr3[] = {3,4,5,6,7};
 	//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
 	int* parr[3] = {arr1, arr2, arr3};
 	int i = 0;
 	int j = 0;
 	for(i=0; i<3; i++)
 	{
 		for(j=0; j<5; j++)
 		{
 			printf("%d ", parr[i][j]);
 		}	
 		printf("\n");
 	}
 	return 0;
}

在这里插入图片描述
parr[i]是parr数组中的元素,通过parr[i]找到的数组元素(arri)又指向了整型⼀维数组,parr[i][j]代表的就是整型一维数组中的元素(arri[j])。
上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的。


3.多种类型的指针变量

在part3我将为大家介绍字符指针变量、数组指针变量、函数指针变量等知识点。


3.1 字符指针变量

在指针的类型中我们知道有一种指针类型为字符指针char *
示例

int main()
{
 	char ch = 'w';
 	char *pc = &ch;
 	*pc = 'c';
 	return 0;
}

将一个字符'w'的地址通过变量ch放入到指针pc中,并进行修改。

示例

int main()
{
 const char* pstr = "hello bit.";     //这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
 printf("%s\n", pstr);
 return 0;
}

其实本质上,是把"hello bit."这个字符串的首字符h的地址存储到了指针变量pstr中。

再给大家看一道《剑指offer》中的笔试题

#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 and str2 are not same
str3 and str4 are same

这里str3和str4指向的是同⼀个常量字符串(不可改变——则无需两个相同的内容,来占用内存)。C/C++会把常量字符串存储到单独的⼀个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
在这里插入图片描述


3.2 数组指针变量

概念

首先我们要明白,数组指针变量,其本质是一种指针变量,存放的是数组的地址,他是能够指向数组的指针变量。
而之前讲到的指针数组,其本质是一种数组,数组中存放的是地址(指针)。

讲到这里请大家分辨一下哪一个是数组指针变量?

int *p1[10];
int (*p2)[10];

结果为

int *p1[10];    //指针数组     看做:数组名为p1,其装有10个int*类型元素的数组
int (*p2)[10];  //数组指针     看做:指针变量名为p2,其指向的内容类型为int[10](大小为10个整形)的数组。

因为[]的优先级要优于*,所以必须要加上()来保证p先和*结合。

数组指针类型解析
在这里插入图片描述

数组指针变量的初始化

int main()
{
	int arr[10] = {0};
	int(*p)[10] = &arr;    //&arr得到的就是数组的地址
	return 0 ;
}
二维数组传参的本质

首先我们理解一下二维数组其实可以看做是每个元素是一维数组的数组,二维数组的首元素就是第一行,是一个一维数组。
在这里插入图片描述
所以,根据数组名是数组首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址。根据上面的图片,第一行的一维数组的类型就是 int [5] ,所以第一行的地址的类型就是要传的首元素的地址的类型,也就是数组指针类型 int(*)[5] 。那就意味着二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参就可以写成数组指针的形式了。

#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
	int i = 0;
	int j = 0;
 	for(i=0; i<r; i++)
 	{
		for(j=0; j<c; j++)
 		{
 			printf("%d ", *(*(p+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}};
 	test(arr, 3, 5);
 	return 0;
}

总结:二维数组传参,参数的部分可以写成数组,也可以写成指针形式。


3.3 函数指针变量

函数指针变量的创建和使用

根据下面的示例我们可以看到,函数也是有地址的,函数名就是函数的地址,我们也可以通过&函数名的方法获得函数的地址。

示例

#include <stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
 	printf("test: %p\n", test);      //%p的作用是打印地址
 	printf("&test: %p\n", &test);
 	return 0;
}

结果为

test: 005913CA
&test: 005913CA

下面将为大家展示如何创建函数指针变量

void test()
{
	printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;

int Add(int x, int y)
{
	return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的

函数指针类型的解析
在这里插入图片描述
相信大家看完这个函数指针类型的解析,对于上面的示例也就清晰明了了。

函数指针的使用
通过函数指针调用指针指向的函数

#include <stdio.h>
int Add(int x, int y)
{
 	return x+y;
}
int main()
{
 	int(*pf3)(int, int) = Add;     //将函数的地址存储到函数指针变量pf3中
 
 	printf("%d\n", (*pf3)(2, 3));  //两种通过函数指针调用函数的表达方式都可以
 	printf("%d\n", pf3(3, 5));
 	return 0;
}

结果为

5
8

4.指针训练

到了part4部分我们就已经学完指针的绝大部分知识点了,下面是我为大家准备了一些数组和指针的简易笔试题,下面的题是通过随记的方式输出的,所以讲解不会非常的详细,跟大家说声抱歉。有需要的可以看一下。

4.1数组和指针的笔试题解析

一维数组

int main()
{
	int a[] = { 1,2,3,4 };//数组有几个元素?

	printf("%zd\n", sizeof(a));//16 -- sizeof(数组名)的场景
	printf("%zd\n", sizeof(a + 0));//a是首元素的地址-类型是int*, a+0 还是首元素的地址,是地址大小就是4/8
	printf("%zd\n", sizeof(*a));//a是首元素的地址,*a就是首元素,大小就是4个字节
	//*a == a[0] == *(a+0)
	printf("%zd\n", sizeof(a + 1));//a是首元素的地址,类型是int*,a+1跳过1个整型,a+1就是第二个元素的地址,4/8
	printf("%zd\n", sizeof(a[1]));//a[1]就是第二个元素,大小4个字节
	printf("%zd\n", sizeof(&a));//&a是数组的地址,数组的地址也是地址,是地址大小就是4/8个字节
	printf("%zd\n", sizeof(*&a));//1. *& 互相抵消了,sizeof(*&a) = sizeof(a) -16
	//2. &a 是数组的地址,类型是int(*)[4],对数组指针解引用访问的是数组, 计算的是数组的大小 -16
	printf("%zd\n", sizeof(&a + 1));//&a+1是跳过整个数组后的那个位置的地址,是地址就是4/8个字节
	printf("%zd\n", sizeof(&a[0])); //首元素的地址,大小4/8个字节
	printf("%zd\n", sizeof(&a[0] + 1));//&a[0] + 1 -- 数组第二个元素的地址,大小是4/8个字节
	return 0;
}

字符数组

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };

	printf("%d\n", sizeof(arr));//数组名单独放在sizeof内部了,计算的是数组的大小,单位是字节-6
	printf("%d\n", sizeof(arr + 0));//arr是数组名表示首元素的地址,arr+0还是首元素的地址,是地址就是4/8个字节
	printf("%d\n", sizeof(*arr));//arr是首元素的地址,*arr就是首元素,大小就是1个字节
	//*arr -- arr[0] - *(arr+0)
	printf("%d\n", sizeof(arr[1]));//arr[1] 是第二个元素,大小也是1个字节
	printf("%d\n", sizeof(&arr));//&arr 是数组地址,数组的地址也是地址,大小是4/8个字节
	//&arr -- char (*)[6]
	printf("%d\n", sizeof(&arr + 1));//&arr+1, 跳过整个数组,指向了数组后边的空间,4/8个字节
	printf("%d\n", sizeof(&arr[0] + 1));//第二个元素的地址,是地址就是4/8字节

	return 0;
}
#include <string.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", strlen(arr));//arr是首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机的
	printf("%d\n", strlen(arr + 0));//arr+0是数组首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机的
	//printf("%d\n", strlen(*arr));//arr是首元素的地址,*arr是首元素,就是'a','a'的ascii码值是97
	//就相当于把97作为地址传递给了strlen,strlen得到的就是野指针, 代码是有问题的
	//printf("%d\n", strlen(arr[1]));//arr[1]--'b'--98,传给strlen函数也是错误的
	printf("%d\n", strlen(&arr));//&arr是数组的地址,起始位置是数组的第一个元素的位置,随机值 x
	printf("%d\n", strlen(&arr + 1));//随机值 x-6
	printf("%d\n", strlen(&arr[0] + 1));//从第2个元素开始向后统计的,得到的也是随机值 x-1
	return 0;
}
int main()
{
	char arr[] = "abcdef";
	printf("%d\n", sizeof(arr));//arr是数组名,单独放在sizeof内部,计算的是数组总大小,是7个字节
	printf("%d\n", sizeof(arr + 0));//arr表示数组首元素的地址,arr+0还是首元素的地址,是地址就是4/8
	printf("%d\n", sizeof(*arr));//arr表示数组首元素的地址,*arr就是首元素,大小是1字节
	printf("%d\n", sizeof(arr[1]));//arr[1]是第二个元素,大小1个字节
	printf("%d\n", sizeof(&arr));//&arr是数组的地址,是地址就是4/8
	printf("%d\n", sizeof(&arr + 1));//&arr是数组的地址,+1跳过整个数组,还是地址,是地址就是4/8个字节
	printf("%d\n", sizeof(&arr[0] + 1));//&arr[0] + 1是第二个元素的地址,大小是4/8个字节
	return 0;
}
int main()
{
	char arr[] = "abcdef";
	printf("%d\n", strlen(arr));//6
	printf("%d\n", strlen(arr + 0));//arr首元素的地址,arr+0还是首元素的地址,向后在\0之前有6个字符
	//printf("%d\n", strlen(*arr));//'a'-97, 出错
	//printf("%d\n", strlen(arr[1]));//'b'-98, 出错
	printf("%d\n", strlen(&arr));//&arr是数组的地址,也是从数组第一个元素开始向后找,6
	//&arr -- char (*)[7]
	//size_t strlen(const char* s);

	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//5

	return 0;
}
int main()
{
	const char * p = "abcdef";

	printf("%d\n", sizeof(p));//p是指针变量,我们计算的是指针变量的大小,4/8个字节
	printf("%d\n", sizeof(p + 1));//p + 1是b的地址,是地址大小就是4/8个字节
	printf("%d\n", sizeof(*p));//p的类型是const char*, *p就是char类型了,1个字节
	printf("%d\n", sizeof(p[0]));//1. p[0] --> *(p+0)--> *p --> 'a',大小是1字节
	//2. 把常量字符串想象成数组,p可以理解为数组名,p[0], 就是首元素

	printf("%d\n", sizeof(&p));//取出的是p的地址,地址的大小是4/8个字节
	printf("%d\n", sizeof(&p + 1));//&p + 1是跳过p指针变量后的地址,是地址就是4/8个字节
	printf("%d\n", sizeof(&p[0] + 1));//4/8 &p[0]-取出字符串首字符的地址,+1是第二个字符的地址,大小是4/8个字节

	return 0;
}
int main()
{
	char* p = "abcdef";
	printf("%d\n", strlen(p));//6
	printf("%d\n", strlen(p + 1));//5
	//printf("%d\n", strlen(*p));//*p就是'a'-97,err
	//printf("%d\n", strlen(p[0]));//p[0]--> *(p+0)--> *p //err
	printf("%d\n", strlen(&p));//&p是指针变量p的地址,和字符串"abcdef"关系就不大了
	//从p这个指针变量的起始位置开始向后数的,p变量存放的地址是什么,不知道,所以答案是随机值
	printf("%d\n", strlen(&p + 1));//随机值

	printf("%d\n", strlen(&p[0] + 1));//&p[0]-取出字符串首字符的地址,+1是第二个字符的地址, 5

	return 0;
}

二维数组

int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));//a是数组名,单独放在sizeof内部,计算的是数组的大小,单位是字节 - 48 = 3*4*sizeof(int)
	printf("%d\n", sizeof(a[0][0]));//a[0][0] 是第一行第一个元素,大小4个字节 
	printf("%d\n", sizeof(a[0]));//a[0]第一行的数组名,数组名单独放在sizeof内部了,计算的是数组的总大小 16 个字节
	printf("%d\n", sizeof(a[0] + 1));//a[0]第一行的数组名,但是a[0]并没有单独放在sizeof内部,所以这里的数组名a[0]就是
	//数组首元素的地址,就是&a[0][0],+1后是a[0][1]的地址,大小是4/8个字节

	printf("%d\n", sizeof(*(a[0] + 1)));//*(a[0] + 1)表示第一行第二个元素,大小就是4
	printf("%d\n", sizeof(a + 1));//a作为数组名并没有单独放在sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,也就是
	//第一行的地址,a+1,跳过一行,指向了第二行,a+1是第二行的地址,a+1是数组指针,是地址大小就是4/8个字节

	printf("%d\n", sizeof(*(a + 1)));//1.a+1是第二行的地址,*(a+1)就是第二行,计算的是第二行的大小 - 16
	//2. *(a + 1) == a[1], a[1]是第二行的数组名,sizeof(*(a + 1))就相当于sizeof(a[1]),意思是把第二行的数组名单独放在
	//sizeof内部,计算的是第二行的大小
	printf("%d\n", sizeof(&a[0] + 1));//a[0]是第一行的数组名,&a[0]取出的就是数组的地址,就是第一行的地址
	//&a[0]+1 就是第二行的地址,是地址大小就是4/8个字节
	printf("%d\n", sizeof(*(&a[0] + 1)));//*(&a[0] + 1)意思是对第二行的地址解引用,访问的就是第二行,大小是16字节
	printf("%d\n", sizeof(*a));//a作为数组名并没有单独放在sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,也就是
	//第一行的地址,*a就是第一行,计算的就是第一行的大小,16字节
	//*a == *(a+0) == a[0]
	printf("%d\n", sizeof(a[3]));//a[3]无需真实存在,仅仅通过类型的推断就能算出长度
	//a[3]是第四行的数组名,单独放在sizeof内部,计算的是第四行的大小,16个字节

	//sizeof(int);//4
	//sizeof(3 + 5);//4

	return 0;
}

指针运算的笔试题解析我会单独写一篇博客与大家分享,敬请期待吧~

  • 58
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值