您现在的位置是:首页 >学无止境 >C语言—指针的进阶网站首页学无止境

C语言—指针的进阶

The August 2023-07-01 16:00:04
简介C语言—指针的进阶

指针的相关概念:

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

字符指针

在指针的类型中知道有一种指针类型为字符指针 char*。

使用方法:

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

	//本质上是把"hello world"这个字符串的首字符的地址存储在了ps中

	char* ps = "hello world";

	//本质是把"hello world"放到arr数组里
	char arr[] = "hello world";


	return 0;
}

注:上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量ps中

经典面试题:

#include <stdio.h>
int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    char *str3 = "hello bit.";
    char *str4 = "hello bit.";
    if(str1 ==str2)
 		printf("str1 and str2 are same
");
    else
 		printf("str1 and str2 are not same
");
       
    if(str3 ==str4)
 		printf("str3 and str4 are same
");
    else
 		printf("str3 and str4 are not same
");
       
    return 0;
}

运行结果:

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

指针数组

指针数组是一个存放指针的数组。

指针数组的定义:

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

指针数组的使用:

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



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));
			printf("%d ", arr[i][j]);
		}
		printf("
");
	}
	return 0;
}

数组指针

数组指针的定义

整形指针: int * pint; 能够指向整形数据的指针。 浮点型指针: float * pf; 能够
指向浮点型数据的指针。数组指针能够指向数组的指针

数组指针的定义:

int main()
{
	int a = 10;
	int*pa = &a;    //整型指针 - 是指向整形的指针
	char ch = 'w';  
	char*pc = &ch;  //字符指着 - 是指向字符的指针


	//数组指针是一种指针 - 是指向数组的指针
	double* d[5];

	double* (*pd)[5] = &d;//pd就是一个数组指针

	int arr[10] = {1,2,3,4,5};
	int (*parr)[10] = &arr;//取出的是数组的地址 

	//parr 就是一个数组指针 - 其中存放的是数组的地址

	//arr - 数组名是首元素的地址 - arr[0]的地址
	return 0;
}

注:

  • double* (*pd)[5]——pd先和 * 结合,说明pd是一个指针变量,然后指针指向的是一个大小为5个double * 类型的指针数组。所以pb是一个指针指向一个数组是数组指针。
  • []的优先级要高于 * 号的,所以必须加上()来保证指针变量先和*结合。

区分&数组名以及数组名

实例一:

在这里插入图片描述

运行结果:数组名和&数组名打印的地址是一样的。

分析:数组名表示的是数组首元素的地址其实&arr和arr,虽然值是一样的,但是意义应该不一样的。实际上 &arr 表示的是数组的地址(取出的是数组的起始地址),而不是数组首元素的地址。此外,arr的类型是int* 的,而&arr的类型是int (*)[10]的。数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.

注:

  • arr是数组名,数组名表示数组首元素的地址。
  • &arr表示取出的是数组的地址
  • 数组名是数组首元素的地址。但是有两个例外:其一是sizeof(数组名) - 数组名表示整个数组,计算的是整个数组大小,单位是字节。其二是&数组名 - 数组名表示整个数组,取出的是整个数组的地址。

数组指针的使用

数组指针指向的是数组,数组指针中存放的应该是数组的地址。

实例一:

在这里插入图片描述

数组指针的应用场景

实例二:

void print1(int arr[3][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 ", arr[i][j]);
		}
		printf("
");
	}
}

//p是一个数组指针
void print2(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));   
			//*(p+i)表示的是第i行的数组名相当于arr[i](第i行的数组名相当于第i行首元素的地址)
			//*(p + i) + j表示的是第i行中下标为j元素的地址
			//*(*(p + i) + j)访问的是第i行第j列的元素
		}
		printf("
");
	}
}

int main()
{
	//int a[5];  &a
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7 } };

	//print1(arr, 3, 5);
	print2(arr, 3, 5);//arr数组名,表示数组首元素的地址
	return 0;
}

注:

  • 二维数组的数组名表示数组首元素的地址
  • 二维数组的首元素是第一行。因为把二维数组理解为一维数组的时候,把它的第一行、第二行、第三行看成一个个元素,这时二维数组才有首元素
  • 二维数组的数组名表示数组中第一行的地址,也就是一维数组的地址
int arr[5][5];
int(*parr)[5][5]=&a;  //parr为二维数组指针

实例三:

int arr[5];      
//整形数组,数组五个元素,每个元素为int类型
int *parr1[10];  
//整形指针数组,数组十个元素,数组的每个元素是int*类型指针 
int (*parr2)[10];
//数组指针,该指针能够指向一个数组,数组十个元素每个元素的类型是int类型
int (*parr3[10])[5];
//parr3是一个存储数组指针的数组,该数组能能够存放10个数组指针,每个数组指针能够指向一个数组,数组5个元素,每个元素是int类型

数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

一维数组传参

#include <stdio.h>
void test(int arr[])//数组传参数组接收,形参部分中的数组大小可以不写,因为本质上传递过来的是数组首元素的地址。形参可以写成数组的形式
{}
void test(int arr[10])//这里的10是没有任何意义的,这种书写方式可以
{}
void test(int *arr)//数组名传参传的是数组首元素的地址
{}
void test2(int *arr[20])//数组传参参数写成数组,因为实参是指针的数组,20可以省略
{}
void test2(int **arr)//数组名表示首元素地址
{}
int main()
{
 	int arr[10] = {0};
 	int *arr2[20] = {0}; //整形指针数组
 	test(arr);
 	test2(arr2);
 	return 0;
}

总结:一维数组传参可以写成一维数组,数组大小可以省略也可以不写,也可以写成指针。如果一维数组是个指针数组也可以写成指针数组或者写成二级指针都可以。

二维数组传参

void test(int arr[3][5])//数组传参实参写成数组形参也是数组
{}
void test(int arr[][])//二维数组行可以省略列不行,这种写法是错误的
{}
void test(int arr[][5])//二维数组中行是可以省略的
{}

//注:二维数组传参可以把参数部分写成数组,行和列可以不省略也可以省略,但是一定只能省略行不能省略列

//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。

void test(int *arr)//用一级指针接收是不行的,这种写法是错误的
{}
void test(int* arr[5])//用指针数组接收是不行的,这种写法是错误的
{}
void test(int (*arr)[5])//实参是二维数组的数组名时,这里写成指针时一定是指向一维数组的指针
{}
void test(int **arr)//实参传递过去的不是二级指针,这种写法是错误的
{}
int main()
{
 	int arr[3][5] = {0};
 	test(arr);//二维数组传参时数组名表示首元素的地址指的是第一行的地址(一维数组的地址)
}

一级指针传参

#include <stdio.h>

void print(int *p, int sz)
{
 	int i = 0;
 	for(i=0; i<sz; i++)
 	{
 		printf("%d
", *(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(p, sz);
 	return 0;
}

注:

  • 一级指针传参一级指针接收
  • 当一个函数的参数部分为一级指针的时候,函数能接收的参数可以是地址(变量和形参所指向的类型是相同的)或者一级指针

实例一:

void test(char* p)
{}

int main()
{
	char ch = 'w';
	char* p = &ch;
	
	//test函数可以接收地址
	test(&ch);//char*
	//test函数可以接收一级指针
	test(p);

	return 0;
}

二级指针传参

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

int main()
{
	int a = 10;
	int* pa = &a;//pa是一级指针
	int** ppa = &pa;//ppa是二级指针
	
	//把二级指针进行传参
	test(ppa);
	printf("%d
", a);//20

	return 0;
}

注:当函数的参数为二级指针的时候,可以接收的参数为二级指针变量、一级指针变量的地址、存放一级指针的数组的数组名

实例一:

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

int main()
{
	int a = 10;
	int* pa = &a;//pa是一级指针
	int** ppa = &pa;//ppa是二级指针
	
	test(ppa);//把二级指针进行传参
	test(&pa);//传一级指针变量的地址

	int* arr[10] = {0};
	test(arr);//传存放一级指针的数组
	printf("%d
", a);//20

	return 0;
}

函数指针

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

函数指针的定义:

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 10;
	int* pa = &a;

	char ch = 'w';
	char* pc = &ch;

	int arr[10] = {0};
	int (*parr)[10] = &arr;//取出数组的地址
	//parr 是指向数组的指针 - 存放的是数组的地址

	//函数指针 - 存放函数地址的指针
	//pf就是一个函数指针变量
	int (*pf)(int, int) = &Add;
	//pf先和*结合,说明pf是指针,指针指向的是一个函数,指向的函数参数有两个并且都是整型类型,返回值类型为int。
	
	//&函数名(函数名) - 取到的就是函数的地址 
	printf("%p
", &Add);
	printf("%p
", Add);

	return 0;
}

注:

  • &函数名和函数名是一个意思代表的含义相同(代表的是函数的地址)
  • 函数调用操作符比间接访问操作符优先级高

补充:函数指针中指针所指向的函数参数部分中的形参名(没有意义)可以省略也可以写上,但形参的类型不可以省略

函数指针的使用:

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	//pf就是一个函数指针变量
	//int (*pf)(int, int) = &Add;

	int (*pf)(int, int) = Add;//Add === pf
	int ret = (*pf)(3, 5);//这种写法中的*是没有意义的(*的个数不限)
	//这个*放在这只是为了理解起来方便,但是没有任何意义(没有实际上的运算意义)
	
	//int ret = pf(3, 5);
	//int ret = Add(3, 5);

	//int ret = * pf(3, 5);//这种写法是err

	printf("%d
", ret);

	return 0;
}

实例一:

(*(void (*)())0)();

注:调用0地址处的函数,该函数无参,返回类型是void。

分析:

  1. void(*)() - 函数指针类型
  2. (void(*)())0 - 对0进行强制类型转换,被解释为一个函数地址
  3. (void()())0 - 对0地址进行了解引用操作
  4. ((void()())0)() - 调用0地址处的函数

实例二:

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

分析:

  1. signal 和()先结合,说明signal是函数名
  2. signal函数的第一个参数的类型是int,第二个参数的类型是函数指针(该函数指针,指向一个参数为int,返回类型是void的函数)
  3. signal函数的返回类型也是一个函数指针(该函数指针,指向一个参数为int,返回类型是void的函数)
  4. signal是一个函数的声明

实例二的简化:

//typedef - 对类型进行重定义(重命名)

typedef void(*pfun_t)(int) ;//对void(*)(int)的函数指针类型重命名为pfun_t
pfun_t signal(int, pfun_t);

注: 函数的返回类型是函数指针类型,其中*必须要和函数名放在一起

函数指针数组

数组是一个存放相同类型数据的存储空间

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

函数指针数组的定义:

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;
	int (*pf2)(int, int) = Sub;
	int (*pfArr[2])(int, int) = {Add, Sub};//pfArr就是函数指针数组
	//int (*pfArr[2])(int, int) = {pf1, pf2};  //也可以这样写
	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 menu()
{
	printf("**************************
");
	printf("**** 1. add    2. sub ****
");
	printf("**** 3. mul    4. div ****
");
	printf("****     0. exit      ****
");
	printf("**************************
");
}

int main()
{
	int input = 0;
	//计算器-计算整型变量的加、减、乘、除
	

	do {
		menu();
		
		int x = 0;
		int y = 0;
		int ret = 0;
		printf("请选择:>");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("ret = %d
", ret);
			break;
		case 2:
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("ret = %d
", ret);
			break;
		case 3:
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("ret = %d
", ret);
			break;
		case 4:
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("ret = %d
", ret);
			break;
		case 0:
			printf("退出程序
");
			break;
		default:
			printf("选择错误,重新选择!
");
			break;
		}
		
	} 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 menu()
{
	printf("**************************
");
	printf("**** 1. add    2. sub ****
");
	printf("**** 3. mul    4. div ****
");
	printf("****     0. exit      ****
");
	printf("**************************
");
}

int main()
{
	int input = 0;
	//计算器-计算整型变量的加、减、乘、除
	//a&b a^b a|b a>>b a<<b a>b

	do {
		menu();

		//pfArr就是函数指针数组
		//pfArr是转移表 

		int (*pfArr[5])(int, int) = { NULL, Add, Sub, Mul, Div };
		int x = 0;
		int y = 0;
		int ret = 0;
		printf("请选择:>");
		scanf("%d", &input);//2

		if (input >= 1 && input <= 4)
		{
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = (pfArr[input])(x, y);
			printf("ret = %d
", ret);
		}
		else if(input == 0)
		{
			printf("退出程序
");
			break;
		}
		else
		{
			printf("选择错误
");
		}
	} while (input);
	return 0;
}


通过下标找到函数指针数组中某个元素,这个元素恰好是一个函数的地址,然后调用这个地址所对应的函数,通常把这样的函数指针数组称为转移表

注: 相比于第一种计算器实现这样写可以避免代码冗余以及增加计算器功能会导致代码量的增大(不便阅读)

指向函数指针数组的指针

指向函数指针数组的指针是一个指针。指针指向一个数组 ,数组的元素都是函数指针。

指向函数指针数组的指针的定义:

void test(const char* str)
{
 	printf("%s
", str);
}
int main()
{
 	//函数指针pfun
 	void (*pfun)(const char*) = test;
 	//函数指针的数组pfunArr
 	void (*pfunArr[5])(const char* str);
 	pfunArr[0] = test;
 	//指向函数指针数组pfunArr的指针ppfunArr
 	void (*(*ppfunArr)[10])(const char*) = &pfunArr;
 	return 0;
}

注:int arr[10]中的数组元素类型时int,arr数组的类型是int[10]

回调函数

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

实例一:

用回调函数实现计算器

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 menu()
{
	printf("**************************
");
	printf("**** 1. add    2. sub ****
");
	printf("**** 3. mul    4. div ****
");
	printf("****     0. exit      ****
");
	printf("**************************
");
}

int Calc(int (*pf)(int, int))
{
	int x = 0;
	int y = 0;
	printf("请输入2个操作数>:");
	scanf("%d %d", &x, &y);
	return pf(x, y);
}

int main()
{
	int input = 0;
	//计算器-计算整型变量的加、减、乘、除

	do {
		menu();
	
		int ret = 0;
		printf("请选择:>");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			ret = Calc(Add);
			printf("ret = %d
", ret);
			break;
		case 2:
			ret = Calc(Sub);
			printf("ret = %d
", ret);
			break;
		case 3:
			ret = Calc(Mul);
			printf("ret = %d
", ret);
			break;
		case 4:
			ret = Calc(Div);
			printf("ret = %d
", ret);
			break;
		case 0:
			printf("退出程序
");
			break;
		default:
			printf("选择错误,重新选择!
");
			break;
		}
		
	} while (input);
	return 0;
}

注:使用回调函数可以减少代码冗余

实例二:

qsort函数的相关介绍:

在这里插入图片描述

void qsort(void* base, //base中存放的是待排序数据中第一个对象(元素)的地址
			size_t num, //排序数据元素的个数
			size_t size,//排序数据中一个元素的大小,单位是字节
			int (*cmp)(const void* , const void* )//是用来比较待排序数据中的2个元素的函数
			);

qsort函数的使用:

//整形数据的排序

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("
");
}

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

void test1()
{
	//整形数据的排序
	int arr[] = { 1,3,5,7,9,2,4,6,8,0};
	int sz = sizeof(arr) / sizeof(arr[0]);
	//排序
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	//打印
	print_arr(arr, sz);
}

//结构体数据的排序

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

int sort_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

int sort_by_name(const void*e1, const void*e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

void test2()
{
	//使用qsort函数排序结构体数据
	struct Stu s[3] = { {"zhangsan", 30},{"lisi", 34},{"wangwu", 20} };

	int sz = sizeof(s) / sizeof(s[0]);
	//按照年龄来排序
	qsort(s, sz, sizeof(s[0]), sort_by_age);
	//按照名字来排序
	qsort(s, sz, sizeof(s[0]), sort_by_name);
}

注:

  • qsort库函数底层用的是快速排序的思想实现的
  • void*的指针是一种无类型的指针,不可以对它进行解引用,什么类型的地址都可以放入到无类型的指针中。
  • strcmp库函数是比较对应位置上的字符的ASCII值
  • void类型的指针作用是用来存放任意指针类型的变量(数据)(任意类型指针的值),但是不可以进行指针解引用操作(因为void是无类型的指针,解引用访问几个字节是不确定的。指针+整数也是一样的道理)。因此要使用void类型的指针就需要提前对该指针进行强制类型转化再进行使用

qsort的模拟实现(采用冒泡排序的思想):

void Swap(char*buf1, char*buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

//使用回调函数,模拟实现qsort(采用冒泡的方式)

void bubble_sort(void* base,int sz,int width,int (*cmp)(const void*e1, const void*e2) )
{
	int i = 0;
	//趟数
	for (i = 0; i < sz - 1; i++)
	{
		//一趟的排序
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			//两个元素比较
			
			if (cmp( (char*)base+j*width, (char*)base+(j+1)*width )>0)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}

指针和数组相关笔试题和面试题

//一维数组
int a[] = {1,2,3,4};
printf("%d
",sizeof(a));      //计算的是数组的总大小  16
printf("%d
",sizeof(a+0));    //计算的是地址的大小-第一个元素地址的大小  4/8
printf("%d
",sizeof(*a));     //计算的是数组元素的大小-数组中第一个元素的大小  4
printf("%d
",sizeof(a+1));    //计算的是地址的大小-第二个元素地址的大小  4/8
printf("%d
",sizeof(a[1]));   //计算的是数组元素的大小-数组中第二个元素的大小  4
printf("%d
",sizeof(&a));     //计算的是地址的大小-整个数组地址的大小  4/8
printf("%d
",sizeof(*&a));    //计算的是数组的总大小(因为&a得到的是数组的地址,对它进行解引用得到的是整个数组的大小。相当于计算sizeof(a)的大小)  16
printf("%d
",sizeof(&a+1));   //计算的是地址的大小-跳过整个数组之后下一块空间的起始地址  4/8
printf("%d
",sizeof(&a[0]));  //计算的是地址的大小-数组中第一个元素地址的大小  4/8
printf("%d
",sizeof(&a[0]+1));//计算的是地址的大小-数组中第二个元素地址的大小  4/8




//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d
", sizeof(arr));           //计算的是数组的总大小  6
printf("%d
", sizeof(arr+0));         //计算的是地址的大小-第一个元素地址的大小  4/8
printf("%d
", sizeof(*arr));          //计算的是数组元素的大小-数组中第一个元素的大小  1
printf("%d
", sizeof(arr[1]));        //计算的是数组元素的大小-数组中第二个元素的大小  1
printf("%d
", sizeof(&arr));          //计算的是地址的大小-整个数组地址的大小  4/8
printf("%d
", sizeof(&arr+1));        //计算的是地址的大小-跳过整个数组之后下一块空间的起始地址  4/8
printf("%d
", sizeof(&arr[0]+1));     //计算的是地址的大小-数组中第二个元素地址的大小  4/8


printf("%d
", strlen(arr));      //从首元素地址开始计算  随机值
printf("%d
", strlen(arr+0));    //从首元素地址开始计算  随机值
printf("%d
", strlen(*arr));     //strlen函数传参时实参不能传字符类型变量(访问内存冲突(编译器)(非法访问内存))(因为strlen函数将传过来的实参强制类型转换成const char*类型的地址,将a的ASCII值97传给strlen参数作为形参的地址)  报错
printf("%d
", strlen(arr[1]));   //strlen函数传参时实参不能传字符类型变量(访问内存冲突(编译器))(因为strlen函数将传过来的实参强制类型转换成const char*类型的地址,将b的ASCII值98传给strlen参数作为形参的地址)  报错
printf("%d
", strlen(&arr));     //在strlen函数看来strlen(&arr)和strlen(arr)以及strlen(arr+0)没有本质区别,将&arr的地址传递给strlen参数,strlen函数会对传递过来的实参进行强制类型转换转换成const char*类型  随机值
printf("%d
", strlen(&arr+1));   //从跳过整个数组之后下一块空间的起始地址开始计算  随机值(随机值-6)
printf("%d
", strlen(&arr[0]+1));//从数组中的第二个元素地址开始计算  随机值(随机值-1)




char arr[] = "abcdef";
printf("%d
", sizeof(arr));       //计算的是数组的总大小  7
printf("%d
", sizeof(arr+0));     //计算的是地址的大小-第一个元素地址的大小  4/8
printf("%d
", sizeof(*arr));      //计算的是数组元素的大小-数组中第一个元素的大小  1
printf("%d
", sizeof(arr[1]));    //计算的是数组元素的大小-数组中第二个元素的大小  1
printf("%d
", sizeof(&arr));      //计算的是地址的大小-整个数组地址的大小  4/8
printf("%d
", sizeof(&arr+1));    //计算的是地址的大小-跳过整个数组之后下一块空间的起始地址  4/8
printf("%d
", sizeof(&arr[0]+1)); //计算的是地址的大小-数组中第二个元素地址的大小  4/8

printf("%d
", strlen(arr));        //从数组中的第一个元素地址开始计算遇到''结束  6
printf("%d
", strlen(arr+0));      //从数组中的第一个元素地址开始计算遇到''结束  6
printf("%d
", strlen(*arr));       //strlen函数传参时实参不能传字符类型变量(访问内存冲突(编译器))(因为strlen函数将传过来的实参强制类型转换成const char*类型的地址,将a的ASCII值97传给strlen参数作为形参的地址)  报错
printf("%d
", strlen(arr[1]));     //strlen函数传参时实参不能传字符类型变量(访问内存冲突(编译器))(因为strlen函数将传过来的实参强制类型转换成const char*类型的地址,将b的ASCII值98传给strlen参数作为形参的地址)  报错
printf("%d
", strlen(&arr));       //在strlen函数看来strlen(&arr)和strlen(arr)以及strlen(arr+0)没有本质区别,将&arr的地址传递给strlen参数,strlen函数会对传递过来的实参进行强制类型转换转换成const char*类型  6
printf("%d
", strlen(&arr+1));     //从跳过整个数组之后下一块空间的起始地址开始计算  随机值
printf("%d
", strlen(&arr[0]+1));  //从数组中的第二个元素地址开始计算遇到''结束  5




char *p = "abcdef";
printf("%d
", sizeof(p));       //计算的是指针变量的大小-计算的是字符类型指针变量的大小(计算的是地址的大小-计算的是字符串中第一个字符地址的大小)  4/8
printf("%d
", sizeof(p+1));     //计算的是地址的大小-计算的是字符串中第二个字符地址的大小  4/8
printf("%d
", sizeof(*p));      //计算的是字符的大小-计算的是字符串中第一个字符的大小  1   
printf("%d
", sizeof(p[0]));    //计算的是字符的大小-计算的是字符串中第一个字符的大小  1   
printf("%d
", sizeof(&p));      //计算的是地址的大小-计算的是字符指针变量p的地址  4/8
printf("%d
", sizeof(&p+1));    //计算的是地址的大小-计算的是从跳过字符指针变量p之后下一块空间的起始地址  4/8
printf("%d
", sizeof(&p[0]+1));    //计算的是地址的大小-计算的是字符串中第二个字符地址的大小  4/8

printf("%d
", strlen(p));      //从字符串中的第一个字符地址开始计算遇到''结束  6
printf("%d
", strlen(p+1));    //从字符串中的第二个字符地址开始计算遇到''结束  5
printf("%d
", strlen(*p));     //strlen函数传参时实参不能传字符类型变量(访问内存冲突(编译器))(因为strlen函数将传过来的实参强制类型转换成const char*类型的地址,将a的ASCII值97传给strlen参数作为形参的地址)  报错
printf("%d
", strlen(p[0]));   //strlen函数传参时实参不能传字符类型变量(访问内存冲突(编译器))(因为strlen函数将传过来的实参强制类型转换成const char*类型的地址,将a的ASCII值97传给strlen参数作为形参的地址)  报错
printf("%d
", strlen(&p));     //从指针变量p的起始地址开始计算  随机值
printf("%d
", strlen(&p+1));   //从跳过字符指针变量p之后下一块空间的起始地址计算  随机值(注:strlen(&p)和strlen(&p+1)这两个随机值完全没有关系,因为根本不知道指针变量p中存放的是什么(指针变量p可能是4字节或者8字节),如果这个指针变量内部就有''那么前面取出得值一定是小于4的,而从跳过字符指针变量p之后下一块空间的起始地址向后遇到的''这个长度有可能会长一些。如果这个指针变量内部没有''那么前面随机值一定长于后面的随机值)
printf("%d
", strlen(&p[0]+1));//从字符串中的第二个字符地址开始计算遇到''结束  5


//二维数组
int a[3][4] = {0};
printf("%d
",sizeof(a));          //计算的是数组的总大小  48
printf("%d
",sizeof(a[0][0]));    //计算的是数组元素的大小-数组中第一行第一列元素的大小  4
printf("%d
",sizeof(a[0]));       //计算的是数组的大小-二维数组中第一行一维数组的总大小  16
printf("%d
",sizeof(a[0]+1));     //计算的是地址的大小-计算的是二维数组中第一行第二列元素的地址的大小(a[0]作为数组名并没有单独放在sizeof内部,也没取地址,所以a[0]表示的就是第一行第一列元素的地址,而a[0]+1表示的就是第一行第二列元素的地址)  4/8          
printf("%d
",sizeof(*(a[0]+1)));  //计算的是数组元素的大小-数组中第一行第二列元素的大小  4
printf("%d
",sizeof(a+1));        //计算的是地址的大小-计算的是二维数组中第二行一维数组的地址的大小(a是二维数组的数组名,并没有取地址也没有单独放在sizeof内部,所以a就表示二维数组首元素的地址即第一行的地址,而a + 1就是二维数组第二行的地址)  4/8
printf("%d
",sizeof(*(a+1)));     //计算的是数组的大小-二维数组中第二行一维数组的总大小(a+1是第二行的地址,所以*(a+1)(a[1])表示的是第二行,因此计算的就是第2行的大小)  16
printf("%d
",sizeof(&a[0]+1));    //计算的是地址的大小-计算的是二维数组中第二行一维数组的地址的大小(a[0]是第一行的数组名,&a[0]取出的就是第一行的地址,&a[0]+1 表示的就是第二行的地址,sizeof(&a[0]+1)和sizeof(a+1)表示的意思一样没有区别)  4/8
printf("%d
",sizeof(*(&a[0]+1))); //计算的是数组的大小-二维数组中第二行一维数组的总大小(&a[0]+1表示的就是第二行的地址,*(&a[0]+1) 就是第二行,所以计算的第二行的大小,sizeof(*(&a[0]+1))和sizeof(*(a+1))表示的意思一样没有区别)  16
printf("%d
",sizeof(*a));         //计算的是数组的大小-二维数组中第一行一维数组的总大小(a作为二维数组的数组名,没有&也没有单独放在sizeof内部,a表示的就是首元素的地址即第一行的地址,所以*a就是第一行,计算的是第一行的大小。*a=*(a+0)=a[0])  16
printf("%d
",sizeof(a[3]));       //计算的是数组的大小-二维数组中第四行一维数组的总大小(a[3]其实是第四行的数组名(如果有的话)所以其实不存在,也能通过类型计算大小的。sizeof()内部的表达式是不会计算的,它会通过表达式的类型属性进行推测从而计算sizeof中表达式的结果,在sizeof(a[3])中a[3]的类型是int[4],sizeof并不会访问a[3]数组从而计算出大小,它会根据a[3]的类型进行推测从而计算出a[3]的大小)  16(并不会报错)   

总结: 数组名的意义:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址。

指针笔试题

实例一:

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

注:&a+1的类型是int(*)[5],将&a + 1的值赋值给int *ptr需要强制类型转换

实例二 :

//已知该结构体的大小是20个字节
struct Test
{
 	int Num;
 	char *pcName;
 	short sDate;
 	char cha[2];
 	short sBa[4];
}*p;

//假设p 的值为0x100000。 以下表表达式的值分别为多少?

int main()
{
 	printf("%p
", p + 0x1);  //0x100014
 	printf("%p
", (unsigned long)p + 0x1);  //0x100001
 	printf("%p
", (unsigned int*)p + 0x1);  //0x100004
 	return 0;
}

注:

  • 指针类型决定了指针的运算(±整数的运算)
  • 第一个表达式是结构体类型指针与整数运算
  • 第二个表达式是将指针类型强制类型转换成无符号的长整型之后再与整数进行运算
  • 第三个表达式是将指针类型强制类型转换成无符号整型指针之后再与整数进行运算

实例三:

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); //4,2000000
    return 0;
}

注:

  • ptr2所指向的位置位于数组首元素的起始地址的下一个字节处
  • %x输出的16进制;%#x可以输出0x的十六进制(不包含高位是零)
  • 每一个字节都给一个地址

实例四:

#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };  //逗号表达式
    //二维数组中放的元素是1 3 5 0 0 0
    int *p;
    p = a[0];
    printf( "%d", p[0]);  //1
 	return 0;
}

注:

  • a[0]代表的是二维数组中第一行数组首元素的地址(第一行第一列的元素地址)

实例五:

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

注:

  • p=a;中a表示的是第一行数组的起始地址,它的类型是int(* )[5]。而p的类型是int(*)[4],这时代码运行时会有类型的差异
  • 指针和指针相减得到的是指针和指针之间元素的个数
  • %p打印地址,打印时认为内存里存的是地址,内存里的补码直接解析成原码

实例六:

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", *(ptr1 - 1), *(ptr2 - 1));  //10,5
    return 0;
}

注:

  • *(aa + 1)表示的是二维数组中第二行数组的首元素的地址
  • int * ptr2 = (int * )(*(aa + 1));中的 (int * )是没有意义,因为 * (aa + 1)表示的就是整型元素的地址不需要强制类型转换再赋值给ptr2指针变量

实例七:

#include <stdio.h>
int main()
{
 	char *a[] = {"work","at","alibaba"};
 	char**pa = a;
 	pa++;
 	printf("%s
", *pa);  //at
 	return 0;
}

在这里插入图片描述

实例八:

int main()
{
 	char *c[] = {"ENTER","NEW","POINT","FIRST"};
 	char**cp[] = {c+3,c+2,c+1,c};
 	char***cpp = cp;
 	printf("%s
", **++cpp);   //POINT
 	printf("%s
", *--*++cpp+3);  //ER
 	printf("%s
", *cpp[-2]+3);  //ST
 	printf("%s
", cpp[-1][-1]+1); //EW
 	return 0;
}

在这里插入图片描述

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。