指针的理解

什么叫指针

简单的理解:内存单元的编号==地址==指针

在指针中,&(取地址操作符)和*(解引用操作符)往往是形影不离的
int main()
{
	int a = 10;
	int* pa = &a;
	printf("%d", *pa);
}

注意:在存放地址的时候必须要用指针来存放,下面我们就引入指针的概念

int a = 10;
int * pa = &a;

上述这段代码表示pa的类型是int*,*说明pa是一个指针变量,而前面的int说明pa指向的是int类型的对象。a的内容是10,而pa指向的是a的地址,也就是可以根据a的地址去找到a的内容

什么叫解引用操作符(*)

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

上述代码就可以看到解引用操作符:*pa的意思就是通过pa存放的地址,找到指向的空间,*pa其实就是变量a;所以*pa = 0,就是把a的值改成了0

指针变量的大小

众所周知,指针变量的大小取决于地址的大小

32位平台下地址是32个比特(bit)位(4个字节)

64为平台下地址是64个比特(bit)位(8个字节)

注意指针变量的大小和类型是无关的,只要指针变量的类型在相同的平台下,大小都是相同的,举个例子:

int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}

x86也就是32位平台,x64也就是64位平台

指针变量类型的意义

指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各种各样的指针类型呢?
其实指针类型是有特殊意义的,我们接下来继续学习。

指针的解引⽤

int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}

根据上述代码我们可以看到,第一段代码会将n的4个字节全部改成0,而第二段代码则只会将n的第一个字节改成0

结论:指针的类型决定了指针解引用的时候一次能够操作几个字节

比如char*的指针解引用只能访问1个字节,而int*的指针解引用则能访问4个字节


指针+-整数

int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}

上述结果为:

上述代码我们不难看出,char*类型的指针变量+1跳过一个字节,int*类型的指针变量+1跳过4个字节。这就是指针变量的类型差异带来的变化。

结论:指针的类型决定了指针向前或者向后走一步有多大距离

void*指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算(ps:具体内容会在后面讲函数参数部分体现)

const

const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤

int main()
{
int m = 0;
m = 20;
//m是可以修改的
const int n = 0;
n = 20;
//n是不能被修改的
return 0;
}

 但是我们绕过n,使用n的地址,去修改n就能做到了,虽然这样做是在打破语法规则

int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}

那么我们如何防止这种事情发生呢?请看下面4段代码:

int main()
{
    int n = 10;
    int m = 20;
    int *p = &n;
    *p = 20;
    p = &m;
}
int main()
{
    int n = 10;
    int m = 20;
    const int* p = &n;
    *p = 20;
    p = &m;
}
int main()
{
    int n = 10;
    int m = 20;
    int *const p = &n;
    *p = 20;
    p = &m;
}
int main()
{
    int n = 10;
    int m = 20;
    int const * const p = &n;
    *p = 20;
    p = &m;
}

上述代码分别是不加const修饰,在*左边加const修饰,在*右边加const修饰 和在*左右两边修饰

结论:const修饰指针变量的时候

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

指针运算

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

指针+-整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p+i));//这里的p+i就是指针加减整数
	}
	return 0;
}

指针+-指针

指针+-指针得到的绝对值是指针和指针之间的元素个数

我们也可以将指针+-指针看做地址+-地址

注意:指针+-指针的前提必须是两个指着指向同一空间

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d", &arr[9] - &arr[0]);
	return 0;
}

 指针的关系运算

指针的关系运算就是指针的大小比较等:

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

野指针

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

野指针的成因

1.指针未初始化

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

2.指针越界访问

int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i <=11; i++)
	{
		*(p++) = i;
	}
	return 0;
}

这里arr只有10个元素,而for循环循环了11次,从而造成了越界访问,当指针指向的范围超出了数组arr的范围时,p就是野指针

3.指针指向的空间释放

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

由于n是在test函数里面定义的,是局部变量,在test函数里面有一块单独的空间,而出了test函数指向的就不在是原来那块空间了,所以返回n的地址的时候就会有问题,所以这里打印p的值的时候是随机值

所以我们在返回的时候的时候千万不要返回局部变量的地址


assert断言

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”

assert(p != NULL);

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣
任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误
流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

如果我们在确认代码没问题的话,我们无须再做断言,那么我们就在#include <assert.h>语句前面,定义一个宏NDEBUG

#define NDEBUG
#include <assert.h>

注意,assert一般是在debug版本使用,如果是在release版本使用的话会直接优化掉


指针的使用和传址调用

strlen的模拟实现

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

int my_strlen(const char* str)
{
	int count = 0;
	while (*str)
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d", len);
	return 0;
}

传值调用和传址调用

首先,让我们看一下下面这段代码

void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}

很明显,交换失败了,那么是为什么呢?让我们一起来一探究竟吧

首先在Swap1函数 里面的x,y有着自己的地址,出了Swap1这个函数,这个空间就返还给了操作空间,所以就无法进行交换,我们将这种称为传值调用

结论:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不会影响实参

那么我们该这么办呢?

我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数Swap函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

void Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}

我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤


数组名的理解:

普通数组名

你知道吗?其实数组名就是地址,而且是首元素的地址,什么你说你不信,那么让我们继续往下看

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];

这里我们使用&arr[0]拿到数组第一个元素的地址,但是其实数组名本来就是地址,而且是首元素的地址,让我们再来写一段代码测试一下吧

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr[0] = %p\n", &arr[0]);
	printf("arr = %p\n", arr);
	return 0;
}

输出结果为: 

我们发现数组名和数组首元素的地址打印出来时一样的,那么我们就可以证明数组名就是数组首元素地址

但是,关于数组名也会有两个例外,接下来请看特殊数组名的理解

特殊数组名

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

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

除了上述两种情况,其他任何地方使用数组名,表示的都是数组首元素的地址

使用指针访问数组

有了前⾯知识的⽀持,再结合数组的特点,我们就可以很⽅便的使⽤指针访问数组了

int main()
{
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		scanf("%d", p + i);
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

在这里呢,数组名arr是首元素地址,可以赋值给p,其实数组名arr和p在这里是等价的

一维数组传参的本质

我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传给⼀个函
数后,函数内部求数组的元素个数吗?请看下面这段代码

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

输出结果为: 

我们发现在函数内部并没有获得正确的元素个数,那么到底是为什么呢?

数组名是数组⾸元素的地址;那么在数组传参
的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组⾸元素的地址 

所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写
sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函
数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

二级指针

一般来说,有两颗*表示的就是二级指针,那么根据这个道理,三颗*表示的就是三级指针

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

**ppa首先通过*ppa找到pa,然后对pa进行解引用操作:*pa,找到a

**ppa的意思:第一颗*表示ppa是int*的类型,第二颗*表示ppa是个指针,指向的是pa的地址

指针数组

指针数组是指针还是数组呢?

我们类比一下,整形数组,是存放整形的数组,字符数组是存放字符的数组。

那么指针数组呢?肯定是存放指针数组的啦!

指针数组的每个元素都是用来存放地址(指针)的,如下如所示: 

指针变量

字符指着变量

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

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

当然,除了单个的字符写法还有另一种,那么就是字符串的写法啦

int main()
{
	char* ptr = "hello world.";
	printf("%s\n", ptr);
	return 0;
}

代码char* ptr = "hello world.";特别容易误以为是把字符串hello world.放到字符指针ptr里面去,但是本质是把字符串hello world.首字符的地址放到了ptr中

数组指针变量

数组指针变量究竟是数组还是指针变量呢?

答:是指针变量

  1. 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
  2. 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。

那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。

 那么问题来了,下面哪个是数组指针变量呢?

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

答:int (*p2)[10]是数组指针变量

解释:p2先和*结合 ,说明p2是一个指针变量,然后指针指向的是一个大小为10个整形的数组。所以p2是一个指针,指向一个数组,叫数组指针

这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p2先和*结合

数组指针变量初始化

数组指针变量是用来存放地址的,那怎么获取数组的地址呢?就需要我们之前学习的&数组名了

int arr[10] = {0};
&arr;//得到的就是数组的地址

 如果要存放个数组的地址,就得存放在数组指针变量中,如下:

int (*p)[10] = &arr;

数组指针类型解析: 

二维数组传参的本质

有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。
过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:

void test(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}};
	test(arr, 3, 5);
	return 0;
}

这⾥实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
⾸先我们再次理解⼀下⼆维数组,⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维
数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。

其实⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址

函数指针变量

什么叫做函数指针变量呢?根据前面的学习,我们不难推出函数指针变量应该是用来存放函数地址的,未来通过地址能够调用函数的。那么函数是否也有地址呢?

我们不妨根据下面这段代码分析一下:

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

运行结果如下: 

由上可见,函数确确实实是有地址的,函数名就是函数的地址,当然也可以通过&函数名的方式获得函数的地址。如果要将函数的地址存放起来,那么我们就得创建函数指针变量咯,函数指针变量 的写法其实和数组指针的写法非常类似,写法如下:

void test()
{
	printf("hehe\n");
}
void(*p1)() = test;
void(*p2)() = &test;
int Add(int x, int y)
{
	return x + y;
}
int(*p3)(int, int) = Add;
int(*p4)(int x, int y) = &Add;//这里形参名x,y可以省略

函数指针类型解析:

函数指针变量去掉函数名剩下的就是他的类型啦! 

typedef关键字

typedef是用来类型重命名的,可以将复杂的类型简单化。是不是有点难懂,不妨我们举个例子吧!

例如:unsigned int这个类型我们可以写成uint,既容易理解又方便,那么怎么实现呢?请看下面这段代码

typedef unsigned int uint

如果是指针类型的又如何重命名呢?其实也是可以的,比如将int*重命名为ptr_t

typedef int* ptr_t

但是对于数组指针和函数指针稍微有些不同,比如将数组指针类型int (*)[5]重命名为ptr_t,可以这样写:

typedef int(*ptr_t)[5]

新的类型名必须写在*的右边而不能直接写在最右,同理,函数指针类型的也一样

函数指针数组

在之前我们学习了指针数组,比如:

int* arr[10]//数组的每个元素是int*

那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

提问:下面三个代码哪个是函数指针数组?

int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];

答:parr1是函数指针数组

因为parr1先和[ ]结合,说明parr1是数组,那么数组的内容是什么呢?

是int(*)()类型的函数指针

sizeof和strlen的对比

sizeof

首先sizeof是计算变量中所占内存空间大小的,单位是字节,如果操作数是类型的话,计算的是类型创建变量所占空间的大小。

注意:sizeof只关注内存空间的大小,不在乎内存中存放的什么数据,比如下面这段代码:

int main()
{
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof a);
printf("%d\n", sizeof(int));
return 0;
}

我们不仅可以根据变量名来计算,还可以通过类型计算,不过要注意的是利用类型计算的时候不能省略左右两边的()

strlen

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

int main()
{
char arr1[3] = {'a', 'b', 'c'};
char arr2[] = "abc";
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
}

strlen和sizeof的对比

sizeofstrlen
1. sizeof是操作符
2. sizeof计算操作数所占内存的⼤⼩,单位是字节
3. 不关注内存中存放什么数据

1.strlen是库函数,需要使用头文件string.h

2.strlen是求字符串长度的,统计的是\0之前字符的个数

3.关注内存中是否有\0,如果没有\0,就会持续往后找,可能会越界

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值