天天看点

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

目录

1. 指针基本知识(指针初阶)

         1.1 指针:

1.2 指针类型

1.2.1 指针

1.2.2 指针类型的意义

1.3 野指针

1.3.1 野指针

1.3.2 野指针的产生:

1.3.3 如何规避野指针

1.4 指针运算

1.4.1 指针+-整数

1.4.2 指针比较大小

1.4.3 指针+-指针

1.5 一级指针

1.6 二级指针

2. 指针进阶

          2.1 字符指针

 2.2 指针数组

2.2.1 指针数组

2.2.2 代码解读

2.3 数组指针

2.3.1 数组指针:

2.3.2 arr和&arr的区别

2.3.3 数组指针的定义

2.3.4 数组指针的应用

2.3.5 代码解读

2.3.6 一维数组传参

2.3.7 二维数组传参

2.4 函数指针

2.4.1 函数指针

2.4.2 函数名和&函数名的区别

2.4.3 函数指针的定义

2.4.4 用函数指针调用函数

2.4.5 代码解读

2.5 函数指针数组

2.5.1 函数指针数组

2.5.2 函数指针数组的定义

2.5.3 函数指针数组的应用

2.6 字节的计算

2.6.1 整型数组

 2.6.2 字符数组

2.6.3 二维数组

2.7 指针的练习

2.7.1 练习1

2.7.2 练习2

2.7.3 练习3

 2.7.4 练习4

2.7.5 练习5

 2.7.6 练习6

1. 指针基本知识(指针初阶)

1.1 指针:

存放地址的变量称为指针。

可以通过指针来访问变量,数组,函数等。

1.2 指针类型

1.2.1 指针

指针是一个变量,那么一定有它自己的类型。指针的类型主要有int*,short*,char*,float*等,还有一些指向数组,结构体,函数等一些对象的类型。

1.2.2 指针类型的意义

1. 指针类型决定了,指针解引用的权限有多大

如:int*类型的指针解引用可以处理四个字节的空间,

而char*类型的指针解引用只能处理一个字节的空间

2. 指针类型决定了,指针走一步,能走多远(步长)

如:

int main()
{
	int a = 3;
	char b = '0';
	int* pa = &a;//定义整型指针
	char* pb = &b;//定义字符指针
	int* pa1 = &a;//定义数组指针
	char* pa2 = &a;
	pa1++;
	pa2++;
	printf("%p\n", &a);
	printf("%p\n", pa1);
	printf("%p\n", pa2);
	return 0;
}
           

运行结果如下:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

 对指针的定义如上述程序所示,分别定义了整型指针和字符型指针,是将所指变量的地址存入指针变量之中。

分别把整型变量a的地址分别存放在整型指针变量pa1和字符型指针变量pa2中,然后对这两个指针变量分别进行加一操作,结果如上图所示。

整型变量a的地址是0x0056F8F4,将a的地址存放到整型指针变量pa1和字符型指针变量pa2中,两个指针变量分别进行加一操作,因为指针变量pa1的类型是int*,所以pa1的一步可以走4个字节,所以pa1+1后的地址是在变量a的地址0x0056F8F4上加了4个字节,即0x0056F8F8。

同理可得,因为指针变量pa2的类型是char*,所以pa2的一步可以走1个字节,所以pa2+1后的地址是在变量a的地址0x0056F8F4上加了4个字节,即0x0056F8F5。

其中%p是打印地址

1.3 野指针

1.3.1 野指针

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

1.3.2 野指针的产生:

1.指针未初始化

2.指针越界访问

3.指针指向的空间释放

1.3.3 如何规避野指针

1.指针初始化

当不知道指针具体地址时,可以将指针初始化为0或者NULL(void *)

2.小心指针越界

3.指针指向空间释放及时置NULL

当指针所指的空间已经还给系统时,将指针置NULL

4.指针使用之前检查有效性

指针置为NULL时也不是有效指针,所以使用前必须检查其有效性

if (p!=NULL)

*p=10;

1.4 指针运算

1.4.1 指针+-整数

指针根据其指针类型,向前或向后移动。

1.4.2 指针比较大小

比较两个指针所存储的地址的大小。

标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针比较,即向前不行,向后可以。

1.4.3 指针+-指针

指针和指针相减的前提是,两个指针指向同一个空间,两个指针相减得到的是两个指针所指的两个空间中间的元素个数。

1.5 一级指针

int arr[3]={1,2,3};
int* p=arr;
arr[2]<==>*(arr+2)<==>*(p+2)<==>*(2+p)<==>*(2+arr)<==>2[arr]
arr[2]==p[2]
           

存在一个数组arr[3],数组名arr表示的是数组首元素的地址,也就是数组元素1的地址,现将数组首元素的地址存放到整型指针变量之中,那么将进行以下的推导:

arr[2]表示的是数组第三个元素,即3,数组名arr表示的是数组首元素的地址,那么arr+2表示的便是数组元素3的地址,再对arr+2进行解引用操作,那么*(arr+2)表示的便是数组元素3,与arr[2]表示的内容一致。

同样,前面将数组首元素的地址存放到整型指针变量p之中,那么整型指针变量p与数组名arr所表示的内容是一样的,所以*(arr+2)与*(p+2)所表示的内容相同,都是数组的第三个元素3。

因为加法存在交换律,那么*(arr+2)与*(p+2)可以写作*(2+arr)与*(2+p),那么就可以写成2[arr]。

根据上述的推导,可以得到arr[2]和p[2]的意义相同,都表示数组的第三个元素3。

1.6 二级指针

int a=10;
int* pa=&a;
int*  *ppa=&pa;
//*pa==a     *ppa==pa ——>**ppa==a
           

如上,将整型变量a的地址存放在一级整型指针变量pa之中,将指针pa的地址再存放在指针ppa之中,那么指针ppa就是二级指针。

对一级指针pa进行解引用操作后,指向的是整型变量a。

对二级指针ppa进行一次解引用操作之后,指向的是一级指针变量pa。

对二级指针ppa进行两次解引用操作之后,指向的便是整型变量a。

2. 指针进阶

2.1 字符指针

char* ps=“hello bit”;//此处的字符串为常量字符串,不能修改
printf(“%s”,ps);//可以打印出整个字符串
           

字符指针可以直接指向一个字符串,如上。其本质上是把字符串的首字符的地址存储到了字符型指针变量ps中,打印的时候可以通过打印字符串全部打印出来。

%s是用来打印字符串的。

2.2 指针数组

2.2.1 指针数组

定义:用来存储指针变量的数组。

指针数组也是数组。

#include<stdio.h>
int main()
{
	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 };//将三个数组的首元素的地址放在指针数组中
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d ", *(arr[i] + j));//指针数组的访问方式1
			printf("%d ", arr[i][j]);//指针数组的访问方式2
		}
		printf("\n");
	}
}
           

如上,定义三个整型数组,将数组首元素的地址放入一个数组arr中,那么这个数组arr就是指针数组,数组类型为int*,里面有三个元素。

arr[i]表示的是指针数组中的第i+1个元素,例如当i=0的时候,arr[0]表示的便是指针数组arr的第一个元素,而第一个元素是a,是数组a的首元素的地址,对首地址元素的地址进行加法操作,就可以得到数组a中其他元素的地址,例如arr[0]+1表示的便是数组a的第二个元素的地址,再对该地址进行解引用操作,便可以得到数组a的第二个元素2,即*(arr[0]+1)==2。所以访问数组元素的时候可以通过指针数组访问,如上,即*(arr[i]+j)。

数组元素通过指针数组来访问还可以用arr[i][j]来访问。该访问方式与二维数组的访问方式相同,但只是模拟出二维数组,实际上与二维数组不同。

2.2.2 代码解读

对已学过的数组类型辨认:

int* arr1[10];
char* arr2[4];
char** arr3[5];
           

第一行是一个整型指针数组,数组里面存放的是10个整型指针;

第二行是一个一级字符型指针数组,数组里面存放的是4个一级字符型指针;

第三行是一个二级字符型指针数组,数组里面存放的是5个二级字符型指针;

2.3 数组指针

2.3.1 数组指针:

指向一个数组的指针变量叫做数组指针。数组指针是一个指针。

2.3.2 arr和&arr的区别

两者的值相同,但意义不一样。

arr表示的是数组首元素的地址,加一之后表示的是数组第二个元素的地址;

&arr是数组的地址,加一之后是跳过了整个数组。

2.3.3 数组指针的定义

int arr[10] = { 1,2,3,4,5 };
int (*parr)[10] = &arr;//arr是数组的首元素的地址,&arr是数组的地址
//parr是一个数组指针,里面存放的是数组的地址
double* d[5];//创建一个指针数组
double* (*pa)[5] = &d;//pa是一个数组指针,里面存放指针数组d的地址
//double*是表示数组里面内容的格式,*pa是表示pa是一个指针,[5]表示所指数组有5个元素
           

首先定义一个整型数组arr,取该数组的地址放入数组parr中,则parr数组指针的类型是int (*)[10],但由于语法要求,就写成如下的形式:int (*parr)[10]。其中*表示parr是一个指针变量;[10]表示parr指向的是一个数组,即表示parr是一个数组指针,10表示parr所指的数组有10个元素;而int表示的是parr所指数组之中的元素的类型都为int类型。

同理可得,先创建一个double*类型的指针数组d,那么创建一个指向该指针数组的数组指针pa,首先取指针数组的地址放入数组指针变量pa之中,而pa的类型需要根据指针数组d来确定。首先pa是一个指针变量,所以表示为(*pa),再者pa所指向的指针数组d有5个元素,所以表示其为(*pa)[5],最后由于指针数组d之中的5个元素都是double*元素,那么就表示为double* (*pa)[5]。

在C语言中,数组名表示的是首元素的地址,但是有两个例外:

1.sizeof(数组名)——数组名表示整个数组,计算的是整个数组的大小,单位是字节;

2.&数组名——数组名表示整个数组,取出的是整个数组的地址。

2.3.4 数组指针的应用

1. 用数组指针访问一维数组

int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int(*parr)[10] = &arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *((*parr) + i));//*parr相当于数组名,也可写作*(*parr + i)
	}
	return 0;
}
           

如上,先创建一个指向整型数组arr的数组指针parr,然后通过parr这个数组指针来访问一维整型数组arr。

parr里面存放的是数组a的地址,那么对parr进行解引用操作,即(*parr),此时表示的是数组名arr,数组名arr表示的是数组arr首元素的地址,所以(*parr)表示的便是数组首元素的地址,相当于数组名arr。要访问整个数组arr的元素,那么就在(*parr)的基础上加上相对于数组首元素地址的偏移量,上述代码的地址相对偏移量为i,故(*parr) + i表示的便是数组arr中第i+1个元素的地址,再对其进行解引用操作,即*((*parr) + i),便可以得到数组arr的第i+1个元素。

2.用数组指针访问二维数组

void print(int(*parr)[5], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)//行
	{
		int j = 0;
		for (j = 0; j < c; j++)//列
		{
			printf("%d ", *(*(parr + 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} };
	print(arr, 3, 5);
	return 0;
}
           

二维数组可以看成几个一维数组组成的数组,例如二维数组arr[3][5],可以看成三个一维数组组成的一个数组,即arr[3]    [5],arr[3]可以看成一维数组的数组名,每个一维数组之中有5个元素。这样看的话,二位数组的首元素为数组的第一个一维数组,也就是二维数组的第一行。而数组名表示的是数组首元素的地址,二维数组也不例外,所以二维数组的数组名表示的就是二维数组第一行元素的地址,也是第一个一维数组的地址,因此arr和arr[0]表示的内容相同,都是二维数组的第一行元素的地址。综上所述,二维数组传递参数时,便可以用一个数组指针接收。

首先定义一个二维数组arr,通过函数print打印整个二维数组,print函数的所传递的参数为二维数组的数组名,二维数组的行数,二维数组的列数,接收的时候用整型数组指针接收二维数组,两个整型变量分别接收二维数组的行数和列数。

二维数组传递的是数组名,接收的是一个数组指针,那么数组名arr与数组指针parr等价,表示的都是二维数组第一行的地址,而parr+i就相当于arr+i,表示的是二维数组的第i+1行的地址,也是第i+1个一维数组的地址,对其解引用操作,即*(parr+i),*(parr+i)相当于该二维数组第i+1个一维数组的数组名,所以也表示为该一维数组的首元素的地址,再通过加上相对于数组首元素地址的偏移量,即*(parr + i) + j,就可以得到二维数组中任何一个元素的地址,再对其进行解引用操作,即 *(*(parr + i) + j),便可以得到二维数组的任何一个元素。

2.3.5 代码解读

指出下列代码表示的内容:

int arr[5];//1
int* parr1[10];//2
int(*parr2)[10];//3
int(*parr3[10])[5];//4
           

1. int arr[5]

表示一个数组,可以存放5个元素,每个元素为int类型。

2. int* parr1[10]

表示一个指针数组,可以存放10个指针,每个指针指向的内容为int类型。

3. int(*parr2)[10]

表示一个数组指针,指向一个数组,数组可以存放10个元素,每个元素为int类型。

4. int(*parr3[10])[5]

表示一个存放数组指针的数组,数组可以存放10个数组指针,每个指针都指向一个数组,每个数组包含5个元素,每个元素为int类型。

2.3.6 一维数组传参

int arr1[10]={0};
int* arr2[10]={0};
test1(arr1);
test2(arr2);

void test1(int arr1[10])
void test1(int arr1[])
void test1(int* arr)
void test2(int* arr[10])
void test2(int**arr)
           

定义一个一维整型数组和一个一维整型指针数组,分别作为函数参数进行传参,有如上5种方式接收。

一维整型数组传参,可以用一维整型数组接收,其中数组元素数量可以省略,也可以用一个一级整型指针接收;

一维整型指针数组传参,可以用一个一维整型指针数组接收,也可以用一个二级整型指针接收。

2.3.7 二维数组传参

int arr [3][5]={0};
int arr [][5]={0};     //行可以省略,列不可以省略
test(arr);

void test(int arr[3][5])
void test(int arr[ ][5])
void test(int (*arr)[5])
           

二维数组作为函数参数进行传参时,可以用一个相同大小的二维数组接收,其中二维数组的行数可以省略,但列数不可以省略;也可以用一个一级数组指针接收(原因见本文2.3.4中第2点)。

2.4 函数指针

2.4.1 函数指针

指向函数的指针 ,存放函数地址的指针叫做函数指针。

2.4.2 函数名和&函数名的区别

数组名!=&数组名(数值相同,意义不同)

函数名==&函数名(数值相同,意义相同)

2.4.3 函数指针的定义

int ADD(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf)(int, int) = &ADD;//pf是一个函数指针
	//int (*pf)(int, int) = ADD;//&ADD==ADD==pf
	printf("%p\n", ADD);
	printf("%p\n", &ADD);//两者意义数字都相同
	return 0;
}
           

有一个函数ADD,定义一个函数指针指向函数ADD。

首先是将函数ADD的地址取出来,存放在函数指针pf之中,*pf表示pf是一个指针,在后面加上小括号,表示这个指针是函数指针,小括号内部是pf所指函数的参数类型,此处pf所指的函数是ADD,其参数有两个,类型都是int类型,所以表示为(*pf)(int, int),而ADD函数还有返回类型,其返回类型为int,所以这个函数指针的定义表示为:int (*pf)(int, int) 。

而后输出函数名和&函数名,观察区别。

运行程序,得到运行结果如下图:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

由此可见, 函数名==&函数名(数值相同,意义相同)。

2.4.4 用函数指针调用函数

int ADD(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf)(int, int) = &ADD;//pf是一个函数指针
	//int (*pf)(int, int) = ADD;//&ADD==ADD==pf
	int ret = (*pf)(3, 5);//第一种:函数指针调用函数
	int ret = pf(3, 5);//第二种:函数指针调用函数,*是为了理解,没有实际的意义
	int ret = ADD(3, 5);//第三种:函数指针调用函数
	printf("%d\n", ret);
	return 0;
}
           

根据2.4.3所述,定义了函数指针,此处用函数指针来调用函数。

第一种:用(*pf)(参数1,参数2)便可以调用ADD函数;

第二种:用pf(参数1,参数2)也可以调用ADD函数;

第三种:是我们常规调用函数的方法,函数名(参数1,参数2)。

由上述三种方法对比,可以得到pf与(*pf)等价,所以函数指针中的解引用操作符*是为了理解才存在的,没有实际意义,可以省略不写。

2.4.5 代码解读

(*(void (*)())0)();//1
void (*signal(int, void(*)(int)))(int);//2
           

1. (*(void (*)())0)();

作用:调用0地址处的函数。

该函数无参数,返回类型为void

从里向外解读如下: 

void(*)()--函数指针类型,void(*p)()--函数指针变量

(void(*)())0--对0进行强制类型转化,转化为函数指针类型

*(void(*)())0--对0地址进行了解引用操作

(*(void(*)())0)()--调用0地址处的函数

2.void (*signal(int, void(*)(int)))(int);

作用:函数声明

等价于   void(*)(int)  signal(int,void(*)(int))

函数signal的函数参数类型分别是int类型和void(*)(int)函数指针类型,返回类型是void(*)(int)

但语法要求不能这样写,所以将函数放*一起

对代码2进行简化,如下

typedef void(*pfun_t)(int);//重新定义void(*)(int)函数指针类型为pfun_t
pfun_t signal(int,void(*)(int));
           

2.5 函数指针数组

2.5.1 函数指针数组

 定义:存放函数指针的数组叫做函数指针数组。

函数指针数组也是数组。

2.5.2 函数指针数组的定义

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int main()
{
	int(*pf1)(int, int) = Add;//定义函数指针变量pf1
	int(*pf2)(int, int) = Sub;//定义函数指针变量pf2
	int(*pfarr[2])(int, int) = { Add,Sub };//pfarr就是函数指针数组
	return 0;
}
           

首先定义两个函数Add和Sub,然后再定义两个函数指针pf1和pf2分别指向函数Add和Sub,再创建一个函数指针数组,创建过程如下。

pfarr是定义的函数指针数组的数组名,该数组里面的元素是函数指针,所以pfarr函数指针数组的类型是int(*)(int, int),[2]表示的是该数组的元素个数,所以定义的函数指针数组为int(*pfarr[2])(int, int)。

2.5.3 函数指针数组的应用

写一个可以实现加减乘除的计算器程序。

第一种:用switch语句实现计算器加减乘除的选择,如下

int add(int x, int y)
{
	return x + y;
}
int sub(int x, int y)
{
	return x - y;
}
int mul(int x, int y)
{
	return x * y;
}
int div(int x, int y)
{
	return x / y;
}
void meau()
{
	printf("*****************************************\n");
	printf("**********   1.add     2.sub   **********\n");
	printf("**********   3.mul     4.div   **********\n");
	printf("**********      0.退出系统     **********\n");
	printf("*****************************************\n");
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		meau();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = add(x, y);
			printf("ret=%d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = sub(x, y);
			printf("ret=%d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = mul(x, y);
			printf("ret=%d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = div(x, y);
			printf("ret=%d\n", ret);
			break;
		case 0:
			printf("退出系统\n");
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
		printf("\n\n");
	} while (input);
	return 0;
}

           

第二种:用函数指针数组实现计算器加减乘除的选择,如下:

int add(int x, int y)
{
	return x + y;
}
int sub(int x, int y)
{
	return x - y;
}
int mul(int x, int y)
{
	return x * y;
}
int div(int x, int y)
{
	return x / y;
}
void meau()
{
	printf("*****************************************\n");
	printf("**********   1.add     2.sub   **********\n");
	printf("**********   3.mul     4.div   **********\n");
	printf("**********      0.退出系统     **********\n");
	printf("*****************************************\n");
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	//转移表
	int(*p[5])(int, int) = { 0,add,sub,mul,div };
	do
	{
		meau();
		printf("请选择:");
		scanf("%d", &input);
		if (input >= 1 && input < 5)
		{
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
			printf("ret=%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出系统\n");
			break;
		}
		else
		{
			printf("输入错误,请重新输入\n");
			break;
		}
		printf("\n\n");
	} while (input);
	return 0;
}
           

第三种:用函数回调的方法实现计算器加减乘除的选择,如下:

//计算器
//3

void cal(int(*p)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0; 
	printf("请输入两个操作数:");
	scanf("%d %d", &x, &y);
	ret = (*p)(x, y);
	printf("ret=%d\n", ret);
}
int add(int x, int y)
{
	return x + y;
}
int sub(int x, int y)
{
	return x - y;
}
int mul(int x, int y)
{
	return x * y;
}
int div(int x, int y)
{
	return x / y;
}
void meau()
{
	printf("*****************************************\n");
	printf("**********   1.add     2.sub   **********\n");
	printf("**********   3.mul     4.div   **********\n");
	printf("**********      0.退出系统     **********\n");
	printf("*****************************************\n");
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		meau();
		printf("请选择:");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			cal(add);
			break;
		case 2:
			cal(sub);
			break;
		case 3:
			cal(mul);
			break;
		case 4:
			cal(div);
			break;
		case 0:
			printf("退出系统\n");
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
		printf("\n\n");
	} while (input);
	return 0;
}
           

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

2.6 字节的计算

2.6.1 整型数组

int main()
{
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));//16--计算的是数组a的长度,有4个元素,为整型类型,4个字节
	printf("%d\n", sizeof(a + 0));//4/8--此处a表示的是数组首元素的地址,计算的是地址的大小
	//32位元素的是4,64位的是8
	printf("%d\n", sizeof(*a));//4--此处计算的是数组的第一个元素的大小,为整型,占4个字节
	printf("%d\n", sizeof(a + 1));//4/8--此处a表示的是数组首元素的地址,计算的是数组第二个元素的地址的大小
	printf("%d\n", sizeof(a[1]));//4--此处计算的是第二个元素的大小,为整型,占4个字节

	printf("%d\n", sizeof(&a));//4/8--此处计算的是数组的地址的大小,但也是地址
	printf("%d\n", sizeof(*&a));
	//16--此处对数组取地址,获得数组地址,解引用后指向的是一个数组,所以此处计算的是数组的大小
	printf("%d\n", sizeof(&a + 1));//4/8--此处计算的是数组之后的空间的地址的大小
	printf("%d\n", sizeof(&a[0]));//4/8--此处计算的是数组第一个元素的地址
	printf("%d\n", sizeof(&a[0] + 1));//4/8--此处计算的是数组第二个元素的地址

	return 0;
}
           

 2.6.2 字符数组

#include<string.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };//空间储存的是 a b c d e f
	printf("%d\n", sizeof(arr));
	//6--数组名单独在sizeof之中,数组名表示的是整个数组,计算的是整个数组的长度,故为6
	printf("%d\n", sizeof(arr + 0));
	//4/8--数组名没有单独在sizeof之中,所以数组名表示的是数组首元素的地址
	//数组首元素的地址加0后,表示的还是数组首元素的地址
	//所以sizeof计算的是数组首元素地址的长度,在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(*arr));
	//1--数组名没有单独在sizeof之中,所以数组名表示的是数组首元素的地址
	//解引用之后就是数组的第一个元素a,所以sizeof计算的是元素a的长度,a为char类型,占一个字节的空间
	printf("%d\n", sizeof(arr[1]));
	//1--同上,sizeof计算的是元素a的长度,a为char类型,占一个字节的空间
	printf("%d\n", sizeof(&arr));
	//4/8--sizeof计算的是数组的地址的大小,因为计算的还是地址的大小
	//所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(&arr + 1));
	//4/8--计算的是数组之后与该数组大小相同的空间的地址的大小,究其本质,计算的还是地址的大小
	//所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(&arr[0] + 1));
	//4/8--计算的是数组第二个元素的地址的大小,所以在32位环境中计算结果为4,在64位的环境下计算结果为8

	printf("%d\n", strlen(arr));
	//随机值--arr表示数组的第一个元素,表示从数组的第一个元素开始计算字符数,直到遇到\0为止
	//但该数组初始化的时候没有存储\0,所以\0的位置不知道在哪里,所以计算结果为随机值
	printf("%d\n", strlen(arr + 0));
	//随机值--同上,也是从数组的第一个元素开始计算字符数,因为\0的位置不知道在哪里,所以计算结果为随机值
	printf("%d\n", strlen(*arr));
	//错误--操作数应该是一个地址,此时操作数为数组首元素a,运行程序的时候会报错
	printf("%d\n", strlen(arr[1]));
	//错误--操作数应该是一个地址,此时操作数为数组元素b,运行程序的时候会报错
	printf("%d\n", strlen(&arr));
	//随机值--从数组的地址开始计算字符数,也就是数组首元素的地址
	//因为\0的位置不知道在哪里,所以计算结果为随机值
	printf("%d\n", strlen(&arr + 1));
	//随机值--从数组之后与该数组大小相同的空间的地址开始计算字符数
	//也就是该数组最后一个元素的地址加一字节的地址开始计算字符数
	//因为\0的位置不知道在哪里,所以计算结果为随机值
	printf("%d\n", strlen(&arr[0] + 1));
	//随机值--从数组第二个元素的地址开始计算,因为\0的位置不知道在哪里,所以计算结果为随机值

	return 0;
}
           
#include<string.h>
int main()
{
	char arr[] = "abcdef";//空间存储的是 a b c d e f \0
	printf("%d\n", sizeof(arr));
	//7--数组名单独在sizeof之中,数组名表示的是整个数组,计算的是整个数组的长度,故为7
	printf("%d\n", sizeof(arr + 0));
	//4/8--数组名没有单独在sizeof之中,所以数组名表示的是数组首元素的地址
	//数组首元素的地址加0后,表示的还是数组首元素的地址
	//所以sizeof计算的是数组首元素地址的长度,在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(*arr));
	//1--数组名没有单独在sizeof之中,所以数组名表示的是数组首元素的地址
	//解引用之后就是数组的第一个元素a,所以sizeof计算的是元素a的长度,a为char类型,占一个字节的空间
	printf("%d\n", sizeof(arr[1]));
	//1--同上,sizeof计算的是元素a的长度,a为char类型,占一个字节的空间
	printf("%d\n", sizeof(&arr));
	//4/8--sizeof计算的是数组的地址的大小,因为计算的还是地址的大小
	//所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(&arr + 1));
	//4/8--计算的是数组之后与该数组大小相同的空间的地址的大小,究其本质,计算的还是地址的大小
    //所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(&arr[0] + 1));
	//4/8--计算的是数组第二个元素的地址的大小,所以在32位环境中计算结果为4,在64位的环境下计算结果为8

	printf("%d\n", strlen(arr));
	//6--arr表示数组的第一个元素,表示从数组的第一个元素开始计算字符数,直到遇到\0为止,\0不计算在内
	printf("%d\n", strlen(arr + 0));
	//6--同上,也是从数组的第一个元素开始计算字符数,所以计算结果为6
	printf("%d\n", strlen(*arr));
	//错误--操作数应该是一个地址,此时操作数为数组首元素a,运行程序的时候会报错
	printf("%d\n", strlen(arr[1]));
	//错误--操作数应该是一个地址,此时操作数为数组元素b,运行程序的时候会报错
	printf("%d\n", strlen(&arr));
	//6--从数组的地址开始计算字符数,也就是数组首元素的地址,所以计算结果为6
	printf("%d\n", strlen(&arr + 1));
	//随机值--从数组之后与该数组大小相同的空间的地址开始计算字符数
	//也就是该数组最后一个元素的地址加一字节的地址开始计算字符数
	//因为\0的位置不知道在哪里,所以计算结果为随机值
	printf("%d\n", strlen(&arr[0] + 1));
	//5--从数组第二个元素的地址开始计算,因为\0的位置不知道在哪里,所以计算结果为5

	return 0;
}
           
#include<string.h>
int main()
{
	char* p = "abcdef";//空间存储的是 a b c d e f \0
	printf("%d\n", sizeof(p));
	//4/8--p为字符型指针变量,里面存储的是字符串的地址,所以计算的是地址的大小
	//所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(p + 1));
	//4/8--p为字符型指针变量,里面存储的是字符串的地址,实际存储的是字符串首元素的地址,即字符a的地址
	//因为计算的是地址的大小,所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(*p));
	//1--p为字符型指针变量,里面存储的是字符串的地址,实际存储的是字符串首元素的地址,即字符a的地址
	//解引用操作后,*p指的是字符a,所以此处计算的是字符a的大小,计算结果为1
	printf("%d\n", sizeof(p[0]));
	//1--同上,p[0]表示的是a,所以此处计算的是字符a的大小,计算结果为1
	printf("%d\n", sizeof(&p));
	//4/8--此处计算的是字符型指针变量p的地址的大小
	//因为计算的是地址的大小,所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(&p + 1));
	//4/8--此处计算的是字符型指针变量p的地址加一字节后地址的大小
	//因为计算的是地址的大小,所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(&p[0] + 1));
	//4/8--此处计算的是字符串第二个字符的地址的大小
	//因为计算的是地址的大小,所以在32位环境中计算结果为4,在64位的环境下计算结果为8

	printf("%d\n", strlen(p));
	//6--p为字符型指针变量,里面存储的是字符串的地址,实际存储的是字符串首元素的地址,即字符a的地址
	//此处从字符a开始计算字符数,遇到\0为止,所以计算结果为6
	printf("%d\n", strlen(p + 1));
	//5--p为字符型指针变量,里面存储的是字符串的地址,实际存储的是字符串首元素的地址,即字符a的地址
	//此处从字符b开始计算字符数,遇到\0为止,所以计算结果为5
	printf("%d\n", strlen(*p));
	//错误--操作数应该是一个地址,此时操作数为字符串首元素a,运行程序的时候会报错
	printf("%d\n", strlen(p[0]));
	//错误--操作数应该是一个地址,此时操作数为字符串首元素a,运行程序的时候会报错
	printf("%d\n", strlen(&p));
	//随机值--此处从字符型指针变量p的地址开始计算字符数,因为\0的位置不知道在哪里,所以计算结果为随机值
	printf("%d\n", strlen(&p + 1));
	//随机值--此处从字符型指针变量p的地址后一字节处开始计算字符数
	//因为\0的位置不知道在哪里,所以计算结果为随机值
	printf("%d\n", strlen(&p[0] + 1));
	//5--此处从字符b开始计算字符数,遇到\0为止,所以计算结果为5

	return 0;
}
           

2.6.3 二维数组

int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));
	//48--数组名单独在sizeof之中,数组名表示的是整个数组,计算的是整个数组的长度
	//整个数组有12个整型元素,一个整型元素占4个字节,所以整个数组的大小为12*4==48
	printf("%d\n", sizeof(a[0][0]));
	//4--此处计算的是二维数组的第一行第一列的元素的大小,该元素为整型,所以计算结果为4
	printf("%d\n", sizeof(a[0]));
	//16--此处计算的是二维数组的首元素的大小,二维数组的首元素为二维数组的第一行
	//第一行有4个元素,每个元素为整型,所以计算结果为4*4==16
	printf("%d\n", sizeof(a[0]+1));
	//4/8--在二维数组中,可以将二维数组的每一行看成一个一维数组,那么a[0]就是二维数组第一个一维数组的数组名
	//而数组名可以表示数组首元素的地址,所以此处计算的是第一行第二列元素的地址的大小
	// 因为计算的是地址的大小,所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(*(a[0]+1)));
	//4--根据上述,(a[0]+1)表示的是二维数组第一行第二列元素的地址,对其解引用,得到的就是二维数组第一行第二列元素
	//此处计算的是二维数组的第一行第二列的元素的大小,该元素为整型,所以计算结果为4
	printf("%d\n", sizeof(a+1));
	//4/8--数组名没有单独在sizeof之中,所以数组名表示的是数组首元素的地址,所以此处计算的是二维数组第二个元素的地址
	// 因为计算的是地址的大小,所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(*(a+1)));
	//16--根据上述,(a+1)表示的是二维数组第二个元素的地址,解引用后得到的是二维数组的第二个元素
	//此处计算的是二维数组的第二个元素的大小,二维数组的第二个元素为二维数组的第二行
	//第二行有4个元素,每个元素为整型,所以计算结果为4*4==16
	printf("%d\n", sizeof(&a[0]+1));
	//4/8--&a[0]表示的是二维数组的首元素的地址,&a[0]+1表示的就是二维数组第二个元素的地址
	// 因为计算的是地址的大小,所以在32位环境中计算结果为4,在64位的环境下计算结果为8
	printf("%d\n", sizeof(*(&a[0]+1)));
	//16--&a[0]表示的是二维数组的首元素的地址,&a[0]+1表示的就是二维数组第二个元素的地址
	//解引用后得到的是二维数组的第二个元素
	//此处计算的是二维数组的第二个元素的大小,二维数组的第二个元素为二维数组的第二行
	//第二行有4个元素,每个元素为整型,所以计算结果为4*4==16
	printf("%d\n", sizeof(*a));
	//16--数组名没有单独在sizeof之中,所以数组名表示的是数组首元素的地址,解引用后得到的是二维数组的首元素
	//此处计算的是二维数组的首元素的大小,二维数组的首元素为二维数组的第一行
	//第一行有4个元素,每个元素为整型,所以计算结果为4*4==16
	printf("%d\n", sizeof(a[3]));
	//16--此处计算的是二维数组的第四个元素,但该二维数组只有三个元素,这属于越界访问
	//但一个变量有两个属性,一个是值数组,一个是类型属性,此处就是根据a[3]的类型属性来计算出的结果
	return 0;
}
           

2.7 指针的练习

2.7.1 练习1

int main()
{
	int a[5] = { 1,2,3,4,5 };
	int* p = (int*)(&a + 1);
	printf("%d,%d", *(a + 1), *(p - 1));
	return 0;
}
           

分析:

该代码首先定义了一个具有5个元素的整型数组a,然后取数组a的地址,加上1,此时的地址指向的是该数组最后一个元素5后面一个整型空间,然后将该地址强制转换为int*类型,存放在整型指针变量p中。然后要打印两个整型数,分别是 *(a + 1)和 *(p - 1)。

第一个:a为数组名,表示数组的首元素的地址,a+1表示的就是数组第二个元素的地址,对其解引用后,得到数组的第二元素,为2,然后打印出2;

第二个:p为整型指针变量,指向的是数组a最后一个元素5后面一个整型空间,p-1表示的是数组的第五个元素的地址,解引用后,得到数组的第五个元素,为5,然后打印出5。

运行程序,得到下图,分析结果无误。

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

2.7.2 练习2

struct test
{
	int num;
	char* name;
	short date;
	char a[2];
	short b[4];
}*p;
//假设p的值为0x100000。
//已知结构体test类型的变量大小是20个字节
int main()
{
	printf("%p\n", p + 0x1);//0x100014
	printf("%p\n", (unsigned long)p + 0x1);//0x100001
	printf("%p\n", (unsigned int*)p + 0x1);//0x100004
	return 0;
}
           

分析:

首先创建了一个结构体类型struct test以及一个结构体指针*p。

根据题目假设,p的值为0x100000,第一个输出的是p+0x1,即p+1,p的类型是结构体指针类型,加一就是加了一个结构体的大小,所以就是加20个字节,写作16进制就是0x100014。

第二个输出:首先将结构体指针类型的p强制转换为unsigned long,那么p就只是一个无符号长整形的变量,加一就是加一,所以输出为0x100001。

第三个输出:首先将结构体指针类型的p强制转换为unsigned int*,那么p加一就是加一个int类型的大小,就是加4个字节,所以输出为0x100001。

2.7.3 练习3

int main()
{
	int a[4] = { 1,2,3,4 };
	int* ptr1 = (int*)(&a + 1);
	int* ptr2 = (int*)((int)a + 1);
	printf("%x %x", ptr1[-1], *ptr2);
	return 0;
}
           

分析:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针
c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

首先定义了一个有4个元素的整型数组a,然后取数组地址加一,加的是整个数组的大小,再将数组指针的类型强制转化为int*类型,赋给整型指针变量ptr1,指向的地址如图所示。

编译环境为小端字节序存储,那么数组a在内存中如图所示存储,a表示的是数组a首元素的地址,如图,强制转化为int类型再加一,就只是整数计算的加一,然后再强制转换为int*类型赋值给整型指针ptr2,所指的地址如图所示。

%x是输出16进制数。

ptr[-1]可以写成*(ptr-1),ptr-1指向的就是数组a中元素4的地址,再进行解引用得到4,按16进制输出4。由上述分析得到ptr2指向的位置,因为ptr2是int*类型,所以向后读取4个字节,解引用得到20000000,按16进制输出为20000000。

运行程序,结果如下图:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

 2.7.4 练习4

int main()
{
	int a[3][2] = { (0,1),(2,3) ,(4,5) };
	int* p;
	p = a[0];
	printf("%d\n", p[0]);
	return 0;
}
           

分析:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

首先是(0,1),逗号表达式的计算结果为1,同理得3和5,因此数组a在内存中的存储如上图。a[0]表示的是二维数组的首元素,即二维数组的第一行,将第一行看作一个一维数组,那么a[0]就是这个一维数组的数组名。p = a[0]就是将一维数组的首元素地址赋值给整型指针p,所以p指向的是二维数组元素1的地址,输出p[0],就是输出*(p+0),结果为1。

程序运行结果如下:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

2.7.5 练习5

int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	printf("%p %d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
	return 0;
}
           

分析:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

a为5行5列的二维数组,存储如图。p为指向有4个元素的整型数组指针,将数组a的首元素地址给p,但p指向的数组最多只有4个元素,所以如上图所示。

根据图可知,&p[4][2] - &a[4][2]计算结果为-4,分别以地址的形式和整型的形式输出。 

-4

原码:10000000 00000000 00000000 00000100

反码:111111111 111111111 111111111 111111011

补码:111111111 111111111 111111111 111111100

运行程序,结果如下:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

 2.7.6 练习6

int main()
{
	int aa[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
	int* ptr1 = (int*)(&aa + 1);
	int* ptr2 = (int*)(*(aa + 1));
	printf("%d %d\n", *(ptr1 - 1), *(ptr2 - 1));
	return 0;
}
           

分析:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

创建一个二维数组aa ,二维数组的存储如图所示,&aa + 1是取二维数组的地址加一,加的是二维数组的大小,再强制转化为int*类型赋值给ptr1,所以ptr1指向的地址如图。*(ptr1 - 1)就是向前移动一个int的大小,即指向元素10,解引用输出10;*(aa + 1)表示的是二维数组首元素的地址加一,也就是二维数组的第二个元素的地址,解引用得到二维数组aa的第二个元素,也就是二维数组的第二行,*(aa + 1)相当于第二行的数组名,表示的是一维数组首元素的地址,即元素6的地址,再强制转化为int*类型赋值给ptr2,指向地址如图所示。*(ptr2 - 1)就是向前移动一个int的大小,即指向元素5,解引用输出5。

运行程序,输出结果如下:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

 2.7.7 练习7

int main()
{
	char* a[] = { "work","at","alibaba" };
	char** p = a;
	p++;
	printf("%s\n", *p);//at
	printf("%c\n", **p);//a
	return 0;
}
           

分析:

字符型指针数组a中存放的是各字符串的地址,也就是个字符串首字母的地址,即字符w的地址,字符a的地址,字符a的地址。将数组a的首元素的地址存放到二级指针p中,即p中是字符w的地址的地址,p++后就是字符串at的首字符a的地址的地址,所以%s打印*p是at,%c打印**p是a。

运行程序,结果如下:

c语言指针应该这么学!1. 指针基本知识(指针初阶)2. 指针进阶2.1 字符指针

继续阅读