C/C++指针详解(超详细教程!!!)

C/C++指针详解(超详细教程!!!)

指针详解1

1. 内存和地址

1.1 内存

在了解内存和地址之前,先讲一个生活的中的案例
假设我们要买一味中药龙胆,医生就会根据中医柜子上的标签来找到龙胆。如果没有标签的话,医生就会一个个找,效率很低。

有了标签,医生可以快速的这味中药。
在这里插入图片描述
上面的例子,相当于我们计算机中的内存单元存放是一样的。

计算器把内存划分为多个的内存单元,每个内存单元的⼤⼩取1个字节。
每个内存单元有相对的编号,如同中医柜子上的标签号。

计算机中常⻅的单位(补充):
⼀个⽐特位可以存储⼀个2进制的位1或者0

在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起了新的名字叫:指针。
所以我们可以理解为:内存单元的编号 == 地址 == 指针

1.2 地址总线

CPU和内存之间有大量的数据进行交互,所以两者必须用线连接起来

在这里插入图片描述
在32位机器上有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有无】,那么⼀根线,就能表⽰2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。

2. 指针变量和地址

2.1 取地址操作符

在C语言中创建变量就是向内存申请空间。比如:
在这里插入图片描述

上述的代码就是创建了整型变量a,内存中申请4个字节,⽤于存放整数10

我们是怎么取到a的地址呢?

用到一个操作符(&)—取地址操作符

	/* 向内存申请空间 存放变量a的值*/
	int a = 10;
	&a;/* 取出a的地址 */

2.2 指针变量和解引用操作符(*)

2.2.1 指针变量

我们通过取地址操作符(&)拿到的地址是⼀个数值,这个数值有时候也是需要
存储起来,⽅便后期再使⽤的,我们把这样的地址值存放在指针变量中

	/* 向内存申请空间 存放变量a的值*/
	int a = 10;
	&a;/* 取出a的地址 */
	int* pa = &a;

以上代码是将a的地址存放到指针变量pa中。

指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的数值都会理解为地址。

2.2.2 理解指针变量
int* pa = &a;

这⾥pa左边写的是 int* , * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int)类型的对象。

char类型变量地址的存放

	char c = 'w';
	&c;/* 取出char类型变量的地址 */
	char* pc = &c;

这⾥pc左边写的是char* , * 是在说明pv是指针变量,⽽前⾯的 char 是在说明pv指向的是字符类型(char)类型的对象。

2.2.3 解引用操作符

上面我们已经知道指针的存放了,那指针该怎么使用呢?
在C语言中,我们我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*)。

char c = 'w';
&c;
char* pc = &c;
*pc = 'u';/* 这里的*就是解引用操作符 */

上述代码中的*pc意思就是通过pc中存放的地址,找到指向的空间,
pc其实就是v变量了;所以pc = ‘u’,这个操作符是把c改成了字符‘u’

2.3 指针变量的大小

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

指针变量的⼤⼩取决于地址的⼤⼩
在32位平台的机器上地址是32bit位,指针大小为4字节。
在64位平台的机器上地址是64bit位,指针大小为8字节。
注意指针变量的大小和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。

3.指针变量类型的意义

3.1指针的解引用

指针的类型决定对指针解引⽤的时候有多⼤的权限(也就是⼀次能访问几个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。

3.2指针加减整数

指针加减整数

char*+1——>跳过char(1)类型个字节
int*+1——>跳过int(4)类型个字节
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)

4.const修饰指针

4.1const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。但是如果这个指针变量被const所修饰就无法改变。这就是const的作用。

4.2const修饰指针变量

const修饰指针变量的时候,只要记住以下两个结论就行:

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

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

5.指针运算

指针的基本运算有三种,分别是:
• 指针± 整数
• 指针-指针
• 指针的关系运算

5.1 指针± 整数

前面已经讲过了,这里就不多赘述。

指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)

5.2 指针-指针

指针减指针是指两个指针相减的操作。当两个指针指向同一数组(或同一块内存)中的不同元素时,可以通过将它们相减来得到它们之间的元素个数(或者是它们之间的偏移量)。

指针减指针,得到的是中间的元素个数(偏移量)

6.野指针

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

6.1造成野指针的原因

1.指针未初始化

2.指针越界访问

3.指针指向的空间释放

6.2如何避免野指针

1.指针初始化

如果不知道指针应该指向哪⾥,可以给指针赋值NULL.NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。

2.避免指针越界访问

3.指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

7.assert断言

assert断言⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”。头文件是#include<assert.h>

assert断言的好处:它不仅能⾃动标识⽂件和出问题的行号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。

#define NDEBUG
#include<assert.h>

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

8.1传值调用和传址调用

传值调用—>把变量本身的值传递给函数,这样无法通过形参来改变实参的值。

实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参。

传址调用—>把变量本身的地址传递给函数,这样可以通过形参来改变实参的值。

指针详解2

1.数组名的理解

1.数组名的理解

数组名本来就是地址,⽽且是数组⾸元素的地址。

int arr[] = { 1,2,3,4,5,6,7 };
printf("    arr = %p\n",arr);/* arr和&arr[0的地址一致 */
printf("&arr[0] = %p\n", &arr[0]);

在这里插入图片描述
数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:
1.sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的大小,单位是字节。
2. &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)。
除此之外,任何地方使用数组名,数组名都表示⾸元素的地址。

int main()
{

	int arr[] = { 1,2,3,4,5,6,7 };
	printf("      arr = %p\n", arr);/* arr和&arr[0的地址一致 */
	printf("    arr+1 = %p\n", arr+1);/* arr和&arr[0的地址一致 */
	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[0] = %zd\n", sizeof(arr[0]));/* 只计算数组中一个元素的大小 */
	printf("      arr = %zd\n", sizeof(arr));/* 计算整个数组的大小 */
	return 0;

}

运行结果如下

在这里插入图片描述

其中&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素的大小。
但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的大小。

2.使用指针访问数组

有了前面的知识,再加上一些的数组的基础知识,我们就可以使用指针访问数组。

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);/* 求元素个数 */
	int* parr = arr;
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		/* 遍历数组 */
		//printf("%d ", *(parr + i));
		printf("%d ",parr[i]);
	}
	return 0;
}

分析:

1.数组名arr是数组⾸元素的地址,可以赋值给parr,其实数组名arr和parr在这⾥是等价的。
2.将*(parr+i)换成parr[i]也是能够正常打印的,所以本质上parr[i] 是等价于 *(parr+i)。
3.同理arr[i] 应该等价于 *(arr+i)

3.一维数组传参的本质

/* void test(int* arr) */ //指针的形式 
void test(int arr[])
{
	int sz2 = sizeof(arr) / sizeof(arr[0]);
	printf("sz2 = %d\n", sz2);
}

int main()
{
	int arr[] = {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;
}

输出结果:
在这里插入图片描述
函数内部没有正确求出数组的大小,我们已经知道:数组名是数组首元素的地址;在数组的传递时把数组的地址给传过去的。本质上数组传递的是数组⾸元素的地址。多所以在函数内部需要一个指针变量来接受首元素的地址。

sizeof(arr) 计算的是⼀个地址的大小(单位字节)并不是数组的大小(单位字节)。
⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

4.冒泡排序算法

冒泡排序的核心思想:两两相邻的元素进行比较

void Bubble_sort(int arr[], int sz)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < sz-1; i++)
	{
		int flag = 1;//假设已经有序
		for (j=0;j<sz-1-i;j++)
		{
			//进行比较
			if (arr[j] > arr[j + 1])
			{
				//进行交换
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				flag = 0;//当flag=0;时,说明是无序的
			}

		}
		if (flag == 1)
		{
			break;//这⼀趟没交换就说明已经有序,后续⽆序排序了
		}
	}
	/* 打印 */
	printf("排序后"); 
	for (i = 0; i < sz; i++)
	{
		printf("%d ",arr[i]);
	}

}

int main()
{
	/* 冒泡排序 */
	int arr[] = {3,2,6,5,1,4,9,8,7,0};
	int sz = sizeof(arr)/sizeof(arr[0]);
	int i = 0;
	printf("排序前");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	Bubble_sort(arr,sz);
	return 0;
}

5.二级指针

指针变量也是变量,是变量就有地址,二级指针用来接收这些地址。

int main()
{
   int a = 100;
   int* pa = &a;
   int** ppa = &pa;
   return 0;
}

对于⼆级指针的运算有:

*ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa , *ppa 其实访问的就是 pa

6.指针数组

指针数组—是存放指针的数组。
数组指针的每个元素都是⽤来存放地址(指针)的。
在这里插入图片描述
数组指针的每个元素是地址,⼜可以指向⼀块区域。

7.指针数组模拟二维数组

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[] = {arr1,arr2,arr3};/* 将三个数组的地址存放指针数组parr中 */

	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]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。

指针详解3

1.字符指针变量

在指针的类型中我们知道有⼀种指针类型为字符指针 char*
下面有一道笔试题:

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和str2分别指向不同的地址,所以str1和str2不同
str3和str4指向的是同⼀个常量字符串(也就是同一篇内存空间)。C/C++会把常量字符串存储到单独的⼀个内存区域,当⼏个指针指向同⼀个字符串的时候,所以str3和str4会指向同⼀块内存。

数组指针变量

2.1数组指针变量概念

1.指针数组—是存放指针的数组。
2.整型指针变量—存放的是整形变量的地址,能够指向整形数据的指针。
3.浮点指针变量—存放浮点型变量的地址,能够指向浮点型数据的指针。
4.数组指针变量—存放的应该是数组的地址,能够指向数组的指针变量。

书写格式:

	int(*pa)[10];

p先和结合,说明p是⼀个指针变量变量,然后指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫 数组指针。
这⾥要注意:[]的优先级要⾼于
号的,所以必须加上()来保证p先和*结合。

2.2数组指针变量初始化

数组指针变量是⽤来存放数组地址的。


int main()
{
	/* 数组指针变量初始化 */
	int arr[10] = {0};
	int(*parr)[10] = &arr;
	return 0;
}

调试:
在这里插入图片描述
调试可以看到&arr和parr是完全相同的。
数组指针类型解析:
在这里插入图片描述

3.二维数组传参的本质

二维数组的传参是这样写的

void test2(int arr[3][5], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ",arr[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} };
	test2(arr, 3, 5);
	return 0;
}

输出结果:
在这里插入图片描述

注意:二维数组的创建和传参行可以省略,但是列不可以省略。

根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。⼆维数组传参本质上也是传递了地址,传递的是第⼀
⾏这个⼀维数组的地址。
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。

4.函数指针变量

函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。

void test3()
{
   ;
}

int main()
{
   printf(" test = %p\n",test);
   printf("&test = %p\n", &test);
   return 0;
}

输出结果:
在这里插入图片描述

函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的方式获得函数的地址。

要把函数的地址存放起来,就要创建函数指针变量。函数指针变量的写法其实和数组指针非常类似。
书写格式:

void (*p)();/* 函数指针变量 */

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

4.2函数指针变量的使用

通过函数指针调⽤指针指向的函数:

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

int main()
{
	int (*p)(int, int) = Add;
	printf("   p = %d\n",p(3,4));
	printf("(*p) = %d\n", (*p)(3, 4));/* 把3,4传参给函数Add */
	return 0;
}

输出结果:
在这里插入图片描述

4.3typedef关键字

typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。

比如 unsigned int 可以写成 uint:

typedef  unsigned int uint;//将unsigned int 重命名为uint

5.函数指针数组

把函数的地址存放到一个数组中,这个就叫函数指针数组。

int(*p[3])();/* 函数指针数组 */

分析:
parr1 先和 [] 结合,说明 parr1是数组,数组是 int (*)() 类型的函数指针。

6.转移表

函数指针数组的⽤途:转移表

void meun()
{
	printf("------------计算器------------\n");
	printf("******** 1.Add   2.Sub *******\n");
	printf("******** 3.Mul   2.Div *******\n");
	printf("********     0.exit    *******\n");
	printf("------------计算器------------\n");

}


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;
}

int main()
{
	/* 转移表 */

	meun();
	int input = 0;
	int x = 0;
	int y = 0;
	int(*parr[5]) (int, int) = { 0,Add,Sub,Mul,Div };
	do 
	{
		printf("请选择:>");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			int ret = parr[input](x, y);//根据input的值来选择函数
			printf("运算结果:%d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else
		{
			printf("输入错误,重新输入\n");
		}
	} while (input);
	return 0;
}

指针详解4

4.1.回调函数

回调函数就是⼀个通过函数指针调⽤的函数。

如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。

4.2.qsort库函数

qsort库函数—可以排序任意类型元素的数据。

对于qsort库函数函数的详解可以看这篇博客:C语言实现用冒泡排序实现qsort函数

4.3.sizeof和strlen的对比

4.3.1sizeof操作符

  1. sizeof 计算变量所占内存内存空间⼤⼩的,单位是字节,如果操作数是类型的话,计算的是使⽤类型创建的变量所占内存空间的大小。
    sizeof 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据。

4 3.2 strlen库函数

  1. strlen 是C语⾔库函数,功能是求字符串⻓度。函数原型如下:
size_t strlen ( const char * str );

strlen统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。strlen 函数会⼀直向后找 \0 字符,直到找到为⽌,所以可能存在越界查找。

4.4sizeof和strlen的对比

4.4.1 sizeof
  1. sizeof是操作符
  2. sizeof计算操作数所占内存的⼤⼩,单位是字节
  3. 不关注内存中存放什么数据
4.4.2 strlen
  1. strlen是库函数,使⽤需要包含头⽂件 string.h
  2. srtlen是求字符串⻓度的,统计的是 \0 之前字符的隔个数
  3. 关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可能
    会越界

数组名的意义(重要)

数组名的意义:

  1. sizeof(数组名),这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩。
  2. &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表⽰⾸元素的地址。

总结

花了近五天的时间,从学习指针到完这篇博客。对于C/C++来说指针不难,其实还是一句话: 当你还在抱怨没有鞋穿,回头看看没有脚的那些人。

学海无涯,永无止境。

希望这篇博客能帮到你。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值