C语言—保姆级指针详解

本文将分为四个大部分,共24个小知识点从零开始详细介绍C语言中的指针,让我们一起开始指针学习之旅吧!

C语言—保姆级指针详解_第1张图片

一.指针入门知识

1.内存和地址

(1)内存

计算机上CPU在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。内存被划分为一个个单元,每个单元的大小是一个字节。每个内存单元都有一个编号,该编号就是地址。

编号=地址=指针

(2)编址

CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,又因为内存中的字节很多,所以需要给内存编址。计算机中的编址并不是把每个字节的地址记录下来,而是通过硬件设计完成的。

32位机器有32根地址总线,每根线只有两种状态,表示0,1。那么一根线就能表示两种含义。32根地址线就能表示32种含义,每一种含义都代表一个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,数据再通过数据总线传入CPU内寄存器。

2.指针变量和地址

(1)&取地址操作符—得到某个量的地址

C语言—保姆级指针详解_第2张图片

&a取出的是a所占4个字节中地址较小的字节的地址。我们只要知道了第一个字节的地址,就可以顺藤摸瓜访问到其他3个字节的数据。

(2)指针变量—我们通过&拿到的地址是一个数值,这个数值就存储在指针变量中。指针变量也是一种变量,这种变量是用来存放地址的。

int a = 10;

int * pa = &a;//*说明pa是指针变量,int说明pa指向的是整型类型的对象

(3)*解引用操作符—使用*可以让我们通过指针找到指针指向的对象

int a = 10;

int* pa =&a;

*pa=0;//*pa就是a变量,这句话把a改成了0

(4)指针变量的大小—取决于一个地址要多大的空间。32位机器一个地址就是32bit,即4个字节。64位机器一个地址就是64bit,即8个字节。

注意!指针变量的大小与类型无关,只要是指针类型的变量,在相同平台下,大小都是相同的。

3.指针变量类型的意义

(1)指针的类型决定了对指针的解引用有多大的权限。

比如:char*的指针解引用就只能访问一个字节,而int*的指针的解引用能访问四个字节。

(2)指针+-整数—指针的类型决定了指针向前或者向后走一步有多大

char*类型的指针变量+1跳过一个字节,int*类型的指针变量+1跳过4个字节。

C语言—保姆级指针详解_第3张图片

(3)void*指针—这种类型的指针可以用来接收任意类型的地址

但是这种类型的指针无法直接进行指针运算。它一般用在函数的参数部分,来接受不同类型的数据的地址,从而实现泛性编程的效果。

4.const修饰指针

(1)const如果放在*左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可以改变。

const int * p   int const * p

(2)const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容可以通过指针来改变。

int *  const p

5.指针运算

(1)指针+-整数

C语言—保姆级指针详解_第4张图片

*(p+i)就相当于*(arr+i)就相当于arr[i]

(2)指针-指针

指针-指针的绝对值是指针和指针之间的元素个数,但前提是两个指针指向的是同一块空间。

C语言—保姆级指针详解_第5张图片

(3)指针的关系运算

C语言—保姆级指针详解_第6张图片

6.野指针

野指针就是指针指向的位置是不可知的。

(1)成因

  指针没有初始化

#include
int main()
{
  int * p;//此处局部变量没有初始化,默认值是随机的
  *p=20;//非法访问内存,指针指向的位置随机
  return 0;
}

  指针越界访问

#include
int main()
{
  int arr[3]={1,2,3};
  int * p = &arr[0];
  for(int i = 0; i < 5;i++)
  {
    printf("%d ",*(p+i));//当i大于2时,指针越界
  }
  return 0;
}

  指针指向的空间释放了

#include
int test()
{
	int n = 10;//n是局部变量,仅在test范围内生效,出了这个函数n就被销毁了。
	return &n;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

(2)规避野指针

  指针初始化:如果明确知道指针指向哪里,据直接赋值。如果不知道指针指向哪里,可以给指针赋值NULL。

  小心指针越界:指针不能超出访问范围

  指针变量不再使用时,及时设置为NULL,指针使用之前检查有效性。

  避免返回局部变量的地址。

7.assert断言

(1)assert.h头文件定义了宏assert(),用于在程序运行时确保程序符合条件,如果不符合,就报错终止运行,这个宏常常被称为断言。

(2)assert(p!= NULL);程序运行到这一句时,验证变量p是否等于NULL。如果确实不等于NULL,程序继续运行,否则就会终止停止运行。

(3)如果已经确定程序没有问题,不需要做断言就在前面定义一个宏NDEBUG

#define NDEBUG
#include 

8.指针的使用和传址调用

(1)strlen的模拟实现

  库函数strlen的功能是求字符串的长度,统计的是字符串中\0之前的字符个数

  函数原型:size_t strlen(const char* str);

#include
size_t my_strlen(const char* str)//传址调用
{
	int count = 0;
	while (*str)
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abdfjh");
	printf("%d", len);
	return 0;
}

(2)传值和传址的区别

  传址调用:可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量。

  传值调用:传值调用函数时,函数的实参传给形参时,形参是实参的一份临时拷贝,形参有自己的一份独立空间,对形参的修改不会影响实参。

  总结:函数中只是需要主调函数中的变量值来实现计算,可以采用传值调用。但是如果函数内部要修改主调函数中变量的值,就需要传址调用。

示例:写一个函数,交换两个整型变量的值

#include 
void swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int main()
{
	int a, b = 0;
	scanf("%d %d", &a, &b);
	printf("交换之前的值是:%d %d\n", a, b);
	swap(&a, &b);
	printf("交换之后的值是:%d %d", a, b);
	return 0;
}

二.深入了解指针

1.数组名的理解

(1)数组名就是数组首元素的地址,但是有两个例外:

   sizeof(数组名):sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。

   &数组名:这里的数组名表示整个数组,取出的是整个数组的地址。

(2)arr与&arr

C语言—保姆级指针详解_第7张图片

&arr[0]和arr都是首元素的地址,+1就是跳过一个元素,即4个字节

&arr是数组的地址,+1是跳过整个数组的

2.使用指针访问数组

  示例:

#include 
int main()
{
	int arr[10] = {0};
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;//p与arr等价
	for (int i = 0; i < sz; i++)
	{
		scanf("%d ", p+i);//相当于&arr[i]
	}
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));//找到地址所指向的内容并打印,等价于*(arr+i)=arr[i]=p[i]
	}
	return 0;
}

3.一维数组传参的本质

数组传参本质上传递的是数组首元素的地址。

一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式,但本质上都是指针

int arr[ ]  //数组形式   int * arr//指针形式

4.冒泡排序

(1)核心思想:两两相邻的元素进行比较,若不满足顺序就交换。一趟冒泡排序使一个数字有序

(2)初级示例:

#include 
void Bubble_sort(int arr[], int sz)
{
	for (int i = 0; i < sz - 1; i++)
	{
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
int main()
{
	int arr[] = {10,9,8,7,6,5,4,3,2,1};
	int sz = sizeof(arr) / sizeof(arr[0]);
	Bubble_sort(arr, sz);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

(3)提高效率的进阶版

  通过引入标记flag来提高效率,如果内循环没有改变flag的值,证明没有发生交换,即这组数已经有序了。

#include 
void Bubble_sort(int arr[], int sz)
{
	for (int i = 0; i < sz - 1; i++)
	{
		int flag = 1;//假设上一趟完成后已经有序
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				flag = 0;//发生了交换,证明这趟还是无序的
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
		if (flag == 1)
			break;
		
	}
}
int main()
{
	int arr[] = {10,9,8,7,6,5,4,3,2,1};
	int sz = sizeof(arr) / sizeof(arr[0]);
	Bubble_sort(arr, sz);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

5.二级指针

(1)定义:在C语言中,二级指针是一个指向另一个指针的指针。它可以用来存储另一个指针变量的地址。这种类型的数据结构通常用于动态多维数组、管理链表和树等数据结构中节点之间的关系。

(2)举例

#include
int main()
{
  int a = 10;
  int * pa = &a;//pa是一级指针
  int **ppa =&pa;//int *说明ppa指向的对象pa的类型是int *,后一个*说明ppa是指针变量
  return0;
}

6.指针数组

(1)定义:指针数组就是存放指针的数组,本质上是数组。指针数组中的每个元素是用来存放指针的。每个元素是地址,又可以指向一块区域。比如:int * arr[4];每个元素是整型指针,所以是指针数组。

(2)举例:指针数组模拟二维数组

#include 
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[3] = { arr1,arr2,arr3 };//数组名是数组首元素的地址,类型是int*的,就可以放在parr数组中
	int i, 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]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。parr[i][j]相当于*(*(parr+i)+j)

 画外音:学到这里已经非常棒啦!!继续加油吧!

C语言—保姆级指针详解_第8张图片

三.指针知识进阶

1.字符指针变量

(1)定义:指向字符串数据的指针变量。每个字符串在内存中都占用一段连续空间,并有唯一确定的首地址。将字符串的首地址赋值给字符指针,可以让字符指针指向一个常量。

  const char * pstr ="hello world";//本质是把字符串hello world的首字符地址放到了pstr中。

(2)举例:

#include 
int main()
{
	char str1[] = "hello";
	char str2[] = "hello";
	const char* str3 = "hello";
	const char* str4 = "hello";
	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和str2是两个独立数组,所以他们的首元素地址肯定不相等。

-str3和str4指向的是同一个常量字符串,C/C++通常会把常量字符串存储到一个单独的内存区域,当几个指针指向同一个字符串时,他们实际会指向同一块内存。

2.数组指针变量

(1)定义:存放数组的地址,能够指向数组的指针变量就是数组指针变量。

   !int * p1[10];//p1是指针数组,数组10个元素,每个元素的类型是int *

       int (*p2)[10];//p2是指针,指针指向的是数组,数组有10个元素,每个元素的类型是int

(2)初始化:int (*p)[10]=&arr;

  p指向的数组的元素类型为int,p是数组指针变量名,[10]是p指向的数组的元素个数

3.二维数组传参的本质

(1)二维数组是元素为一维数组的数组,对于二维数组,首元素就是第一行,首元素的地址就是第一行的地址。

(2)二维数组传参本质上是传递了地址,传递的是第一行这个一维数组的地址。二维数组传参,形参的部分可以写成数组,也可以写成指针。

#include 
void test(int(*p)[5], int r, int c)
{
	for (int i = 0; i < r; i++)
	{
		for (int 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;
}

  上述代码可以打印出arr这个二维数组。在函数部分,在i=0,j=1的情况下,*(p+i)表示的是二维数组的第一行,*(p+i)+j是第一行的第二个元素,再加一个*表示解引用到这个元素。

四.函数指针变量

(1)定义:函数是有地址的,函数名就是函数的地址,也可以通过&函数名来获得函数的地址,函数指针变量是用来存放函数的地址的,将来能够通过地址来调用函数。

int (*p) (int x, int y)  //p是函数指针变量名,最前面的int是p指向的函数的返回类型,()内部的是p指向的函数的参数类型和个数。

(2)举例:通过函数指针调用指针指向的函数

#include 
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int(*pf3)(int, int) = Add;
	printf("%d\n", (*pf3)(2, 3));//解引用找到Add函数
	printf("%d\n", pf3(2, 3));//函数名就代表函数地址,pf3与Add这一函数名等价
	return 0;
}

(3)typedef关键字

  typedef关键字是用来类型重命名的,可以将复杂的类型简单化。

​
typedef unsigned int unit;//将unsigned int重命名为unit
typedef int* ptr;//将int*重命名为ptr
typedef int(*parr_t)[5];//将数组指针类型int(*)[5]重命名为parr_t 
重命名数组指针类型,新的类型名必须在*右边
typedef void(*pfun_t)(int);//将函数指针类型void (*)(int)重命名为pfun_t 
重命名函数指针类型,新的类型名也必须在*右边

​

5.函数指针数组

(1)定义:把函数的地址存放到一个数组里,这个数组就叫函数指针数组。

(2)int (*parr1[3])(); parr1先和[]结合,说明parr1是数组,数组的内容是int (*)()类型的指针。

(3)应用:转移表实现整数加减乘除计算器

  转移表是一种用于实现快速的条件条件分支选择的技术,通常使用指针数组来访问代码块。

#include 
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("*******************\n");
	printf("***请输入你的选择**\n");
	printf("***1.Add   2.Sub***\n");
	printf("***3.Mul   4.Div***\n");
	printf("***0.exit       ***\n");

}
int main()
{
	int input = 0;
	int x, y,ret = 0;
	int(*p[5])(int x, int y) = { 0,Add,Sub,Mul,Div };
	do
	{
		menu();
		scanf("%d", &input);
		if ((input >= 1) && (input <= 4))
		{
			printf("请输入操作数:");
			int x, y = 0;
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
			printf("计算结果是%d", ret);
			printf("\n");
		}
		else if (input == 0)
		{
			printf("计算结束\n");
			break;
		}
		else
		{
			printf("非法输入\n");
		}
	} while (input);
	return 0;
}

四.指针习题训练

画外音:前面我们已经学习了许多关于指针的知识,接下来让我们分析几道习题来巩固一下吧~

C语言—保姆级指针详解_第9张图片

(1)sizeof和strlen的对比

  sizeof:sizeof这一操作符是计算变量所占内存空间大小的的,单位是字节,sizeof只关注占用内存空间的大小,不在乎内存中存放什么数据。

  strlen:strlen是C语言库函数,功能是求字符串的长度,函数原型为:size_t strlen(const char* str),strlen统计的是参数str中这个地址开始向后,\0之前字符串中字符的个数。strlen函数会一直向后找\0字符,直到找到为止,所以可能存在越界访问。

  接下来我们来看一道习题:

#include
int main()
{
	char* p = "abcdef";
	printf("%zd\n", sizeof(p + 1));
	printf("%zd\n", sizeof(&p[0] + 1));
	printf("%zd\n", sizeof(*p));
	printf("%d\n", strlen(p + 1));
	printf("%d\n", strlen(&p[0]+1));
	printf("%d\n", strlen(p[0]));
}

—p是首元素地址,p+1就是第二个元素的地址,所以sizeof(p+1)就是求b的地址的大小,结果是4或者8个字节

—&p[0]是a的地址,加上1就是b的地址,结果还是4或者8个字节

—p是首元素a的地址,*p就是a,sizeof(*p)等同于sizeof('a'),求字符a所占的空间,结果为1字节

—p表示第一个元素的a的地址,p+1就是b的地址,strlen(p+1)代表从b开始往后找\0要走几步,答案是5

—&p[0]就是字符a的地址,加上1就是字符b的地址,strlen(&p[0]+1)代表从b开始往后找\0要走几步,答案是5

—p[0]是字符a,但是strlen接收的是字符指针,类型不符合会出错。

 (2)指针练习题:

题目一:

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

—&a取出的是整个数组的地址,那么&a+1就跳过了整个数组,指针ptr就指向了5后面的位置,又因为ptr的类型是int* 所以ptr-1往回走一个整型,指向5,*(ptr-1)就是5

a是数组首元素1的地址,a+1是2的地址,所以*(a+1)的值是2

—打印结果为2 5

题目二:

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

—数组用逗号表达式初始化,逗号表达式从左向右计算结果为最右边的值,其初始化相当于int a[3][2] = {1,3,5};故二维数组前三个元素为1,3,5后面三个元素为0

—a[0]是首元素地址,p[0]相当于*(p+0)==*(a+0)即首元素1,结果为1

题目三:

#include
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));
	return 0;
}

—&aa是整个数组的地址,&aa+1跳过了整个数组,指向10后面的位置,又因为ptr1的类型是int*,所以ptr1-1往回走四个字节,指向10,*(ptr1-1)打印10

—aa是数组首元素的地址,即第一行的地址,aa+1就是第二行的地址,指向6,又因为ptr2的类型是int*,所以ptr2-1往回走4个字节,指向5,*(ptr2-1)打印5

尾声:指针的介绍到这里就走到尾声了,对指针感兴趣的朋友们可以看一看《Pointers on C》这本书,这本书全面而详细地介绍了指针,对于初学者和有经验的程序员都很有用~

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