指针(一)

目录

1.内存和地址

1.1 内存

1.2 编址

2.指针变量和地址

 2.1 取地址操作符(&)

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

2.2.1 指针变量

2.2.2 如何拆解指针类型 

2.2.3 解引用操作符

2.3 指针变量的大小

3.指针变量类型的意义

3.1 指针的解引用

3.2 指针+-整数

 ​编辑

3.3 void* 指针

4.const修饰指针

4.1 const修饰变量

5.指针运算

5.1 指针+-整数

5.2 指针-指针(地址-地址)

 5.3 指针的关系运算

6.野指针

6.1 野指针成因

6.2 如何避免野指针

6.2.1 指针初始化

6.2.2 小心指针越界

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

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

7.assert断言

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

8.1 strlen 的模拟实现

 8.2 传值调用和传址调用


1.内存和地址

1.1 内存

生活中,一栋楼的各个房间都会有它的编号,比如:一楼有101,102,103……二楼有201,202,203……有了房间号,我可以快速地找到我要去的房间,提高效率。

同理,在计算机中:计算机上的CPU(中央处理器)在处理数据是,需要的数据是在内存中读取的,处理后的数据也会放回内存中。

人们将内存划分为一个个的内存单元,每个内存单元的大小取一个字节。(1byte = 8bit)

其中,每个内存单元相当于一个宿舍,一个字节空间里存放八个比特,相于八人间,每一个人是一个比特位。

每个内存单元也都有一个编号(这个编号相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU可以快速找到一个内存空间。

生活中我们把门牌号也叫做地址,在计算机中我们把内存单元的编号也称为地址,C语言中给地址起了新名字叫做:指针

所以我们可以理解为:

内存单元的编号 == 地址 == 指针

1.2 编址

首先,计算机有很多硬件单元,而硬件单元之间要相互协同工作的,而协同,至少相互之间要能够进行数据传递。我们通过“线”将硬件和硬件连接起来。而CPU和内存之间也是有大量的数据交互的,所以两者之间也必须用“线”连接起来。

指针(一)_第1张图片

CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址(就如同宿舍门牌号)。

计算机中的编址,并不是把每一个字节的地址记录下来,而是通过硬件设计完成的。

可以这么理解,32为机器有32根地址总线,每一根都只有两种状态,表示为0和1,那么一根线就能表示两种含义,32根线就能表示2^32种含义,每一种含义都代表一个地址。

地址的信息被下达给内存,在内存上,就可以找到该地址的对应的数据,将数据在通过数据总线传入CPU内寄存器。

所以可知,内存被划分为一个个的内存单元,一个内存单元的大小是1个字节。每个内存单元都会有一个编号,这个编号就是地址,C语言中把地址又称为“指针”。即编号==地址==指针。

2.指针变量和地址

 2.1 取地址操作符(&)

在C语言当中创建变量其实就是向内存申请空间。 

int main()
{
	int a = 10;
	printf("%p", &a);
	return 0;
}

这段代码的表面意义是创建一个变量 a,并赋值为10。它的深层意义是在内存上申请4个字节的空间,存放10。

为了便于观察每个字节的地址,int a  = 0x11223344

指针(一)_第2张图片

上图中4个字节的地址分别是:

0x006818A2 

0x006818A3

0x006818A4

0x006818A5

a的地址是地址较小的字节的地址(比如上图的44的地址) ,只要知道了第一个字节的地址,就可以访问到4个字节的数据。

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

2.2.1 指针变量

我们通过取地址操作符得到的地址是一个数值,比如:0x006818A2 ,如果想在后续使用它,就需要将它存放在指针变量中。

int main()
{
	int a = 0x11223344;
	printf("%p", &a);
	int* pa = &a;//pa是指针变量 - 存放地址
	return 0;
}

指针变量也是一种变量,存放在指针变量中的数值都会理解为地址。

2.2.2 如何拆解指针类型 

这里的int* pa = &a int 指的是pa指向的对象是int类型的* 指的是说明pa是指针变量

int *pa

int* pa

int * pa

是一样的 

指针 和 指针变量

地址      变量 - 存放地址   (口头说的指针一般是指针变量)

2.2.3 解引用操作符

我们将 地址保存起来,后续怎么使用呢?

在现实生活中,我们使用地址要找到一个房间,在房间里可以拿去和存放物品。

在C语言中,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里引入一个操作符叫解引用操作符(*)

指针(一)_第3张图片

上面代码的第七行使用了解引用操作符(*),*pa指的是通过pa存放的地址找到它所指的对象a,所以*pa就等价于a。第八行中*pa = 20,就等于将a的值改成20。

通过pa来修改a的值,对a的修改多了一种途径,写代码就会更加灵活。

2.3 指针变量的大小

指针变量是专门用来存放地址的 ,放在里头的任何东西都看作地址。指针变量的大小取决于一个地址的存放需要多大的空间。

在前面讲到的,32位机器假设有32根地址线,每根地址线出来的电信号会转换为1或0。那么我们将32根地址线产生的2进制序列当作一个地址,那么一个地址就是32个bit位,需要4个字节才能储存。

如果使用指针变量用来存放地址,那么指针变量的大小就得是4个字节的空间。

同理的,64位机器,假设有64根地址线,一个地址就是64个bit位,需要8个字节的空间,指针变量的大侠就是8个字节。

#include 
//指针变量的大小取决于地址的大小
//X86下地址是32个bit位
//X64下地址是64个bit位
int main()
{
	char* pa = NULL;
	short* pb = NULL;
	int* pc = NULL;
	double* pd = NULL;
	printf("%zd\n", sizeof(pa));
	printf("%zd\n", sizeof(pb));
	printf("%zd\n", sizeof(pc));
	printf("%zd\n", sizeof(pd));
	return 0;
}

指针(一)_第4张图片 指针(一)_第5张图片

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

3.指针变量类型的意义

指针变量的大小和类型是无关的,只要指针类型的变量在相同的平台下,大小都是相同的,为什么还要有各种各样类型的指针变量呢?

3.1 指针的解引用

 对比下面两段代码,观察其调试时的内存变化。

#include 

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

调试如下: 

指针(一)_第6张图片

指针(一)_第7张图片

4个字节全都改为0 。

如果将 int 类型改成 char 类型呢? 

#include 

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

 调试如下:指针(一)_第8张图片

 只修改了第一个字节。

由此可知,指针的类型决定了,对指针解引用的时候有多大的权限(一次可以操作几个字节)。

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

3.2 指针+-整数

 指针(一)_第9张图片

我们可以看出, char* 类型的指针变量+1跳过1个字节,int* 类型的指针变量+1跳过了4个字节,这就是指针变量的类型差异带来的变化。

所以,指针的类型决定了指针向前或向后走一步有多大。

3.3 void* 指针

在指针类型中有一种特殊的类型是 void* 类型,可以理解为无具体的指针(或者叫泛型指针)。这种指针可以用来接受收任意地址,但是也有局限性,void* 类型的指针不能直接进行指针的+-整数和解引用的运算

#include 

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

这种写法将一个 int 类型的变量的地址赋给了一个 char* 类型的指针变量,编译器给了如下警告,是因为类型不兼容。 

指针(一)_第10张图片

而使用 void* 类型就不会报警。

指针(一)_第11张图片

void* 类型是无法进行解引用运算和指针+-整数运算的:

#include 

int main()
{
	int a = 10;
	void* pc = &a;
	*pc = 10;
	pc++;
	return 0;
}

指针(一)_第12张图片

根据 void* 指针的性质,可以把它看成是一个“垃圾桶”,什么都能往里放,但是它无法进行其他指针可以的运算。

那么 void* 类型的指针有什么用呢?

一般的,void* 类型的指针式使用在函数参数的部分,用来接受不同的类型的数据地址,这样的设计可以实现泛型编程的效果。

4.const修饰指针

4.1 const修饰变量

变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量。

但是如果我们希望一个变量加上一些限制,不能被修改,这就要用到const了。

指针(一)_第13张图片

a的值不能被修改了,但是a的本质还是变量,const仅仅式在语法上做了限制,所以我们习惯上叫a是常变量。

“进不了正门,那就爬窗户”。我们绕过n,使用n的地址去修改n,虽然可以,但是“爬窗户”的行为是危险的。

指针(一)_第14张图片

const修饰指针的时候,const可以放在左边,也可以放在右边,两种写法的作用不同。

1.p里边存放的是地址(a的地址);

2.p是变量,有自己的地址;

3.*p是p指向的一块空间。

#include 

int main()
{
	const int a = 10;
	int const* p = &a;//限制的是*p
	*p = 0;//err      //指的是:不能通过p来修改p指向的空间的内容
    int b = 20;
    p = &b;//ok
	printf("%a = %d\n", a);
	return 0;
}

const放在 * 的左边限制的是 *p ,意思是不能通过指针变量p来修改p指向的空间的内容,但是p不受限制;

#include 

int main()
{
	const int a = 10;
	int *const p = &a;const限制的是p
	*p = 0;//ok
	int b = 20;
	p = &b;//err
	printf("%a = %d\n", a);
	return 0;
}

const放在 * 的右边限制的是p ,意思是p变量不能被修改,没办法再指向其他的变量了,但是*p 不受限制,还是可以通过p来修改p所指向的空间的内容。

5.指针运算

指针的基本运算有三种,分别是:

指针+-整数

指针-指针

指针的关系运算

5.1 指针+-整数

因为数组再内存中是连续存放的,只要知道第一个元素的地址,就能找到后面的所有元素。

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

指针(一)_第15张图片

也可以这么写: 

指针(一)_第16张图片

5.2 指针-指针(地址-地址)

指针-指针运算的前提条件是:两个指针指向统一块空间。 

 指针-指针的绝对值指针和指针之间的元素的个数

指针(一)_第17张图片

指针(一)_第18张图片

指针-指针到底有什么作用呢?

这里要补充以下strlen的知识点:

把一串字符串传给strlen的时候,传的不是字符串本身,而是首个字符的地址。不统计\0。

指针(一)_第19张图片

 如果我要实现strlen这样的函数:

#include 

int my_strlen(char* s)
{
	int count = 0;
	while (*s != '\0')
	{
		count++;
		s++;
	}
	return count;
}

int main()
{
	int len = my_strlen("abc");
	printf("%d\n", len);
	return 0;
}

结合指针-指针的思想:

#include 

int my_strlen(char* s)
{
	char* start = s;
	while (*s != '\0')
	{
		s++;
	}
	return s - start;
}

int main()
{
	int len = my_strlen("abc");
	printf("%d\n", len);
	return 0;
}

 5.3 指针的关系运算

指针的大小比较: 

指针(一)_第20张图片

注意:arr是数组名,数组名其实就是数组的首元素的地址 

6.野指针

概念:野指针就是指针指向的位置是不可知的、随机的 。

6.1 野指针成因

(1)指针未初始化:

全局变量静态变量如果不初始化,变量的默认值是0

局部变量如果不初始化,变量的值是随机的!

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 = 1;
		p++;
	}
	return 0;
}

 (3)指针的空间释放

int* test()
{
	int n = 100;
	return &n;
}

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

这里的n在出test()就销毁了,访问不到。 

6.2 如何避免野指针

6.2.1 指针初始化

如果明确知道指针指向哪里就直接复制地址,如果不知道,可以给指针赋值NULL。NULL是C语言常见的标识符常量,其本质是0。

int main()
{
	int num = 20;
	int* p = #
	int* pa = NULL;
	return 0;
}

6.2.2 小心指针越界

一个程序向内存申请了哪些空间,通过指针也只能访问那些空间,不能超出范围。

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

当指针变量指向一块区域时,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。只要是NULL指针就不去访问。

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

比如上面的(3)

7.assert断言

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

assert(p! = NULL);

程序在运行到这一行语句时,验证变量p是否等于NULL,如果不等于NULL,程序运行,否则终止运行并报错。 

指针(一)_第21张图片

assert() 宏接受一个表达式最为参数,如果该表达式为真(返回值非零),assert() 时不起作用的,程序正常运行下去。如果表达式为假(返回值为零),assert() 就会报错,在标准错误流 stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

 使用assert()的好处有:它不仅能自动标识文件出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果已经确认了程序没有错误,就可以#define 定义一个宏 NDEBUG

然后,重新编译程序,编译器就会禁止文件中所有的 assert() 语句。

指针(一)_第22张图片

如上图,加上 NDEBUG,即使 p = NULL ,程序依然运行没有报错。 

如果程序又出现问题,可以移除或者注释掉 #define NDEBUG 指令,再次编译,这样就重新启用了 assert() 语句。

因为引入了额外的检查,增加了程序的运行时间。

一般地,我们在 Debug 中使用 assert(),在 Release 中禁用 assert() 语句。这样在 Debug 版本中有利于程序员排查问题,在 Release 版本中不影响用户的使用效率。

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

8.1 strlen 的模拟实现

详见5.2 指针-指针(地址-地址)

 8.2 传值调用和传址调用

传值调用:

#include 

Swap(int x, int y)
{
	int z = 0;
	z = x;
	x = y;
	y = z;
}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);

	printf("交换前:a=%d b=%d\n", a, b);
	Swap(a, b);
	printf("交换后:a=%d b=%d\n", a, b);

	return 0;
}

指针(一)_第23张图片

这样的一个程序本意上是想通过Swap()函数实现数值的交换,但是输出的结果发现,交换前后不变。通过监视窗口可以看到:指针(一)_第24张图片

 函数的实参传给形参时,形参是实参的一份临时拷贝。对形参的修改不影响实参!

实参a和b有自己的地址,形参x和y也有自己的地址。

当进入Swap()函数后经过的一通交换都是在x和y的地址上进行的,并没有影响到实参a和b。

当出了这个函数,回到现实,a和b依然是赋给的值10和20。

所以想要改变函数外边的实参就需要传址调用:

#include 

Swap(int *pa, int *pb)
{
	int z = 0;
	z = *pa;
	*pa = *pb;
	*pb = z;
}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);

	printf("交换前:a=%d b=%d\n", a, b);
	Swap(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);

	return 0;
}

这次将变量的地址传给了Swap()函数。

指针(一)_第25张图片

指针(一)_第26张图片 传址调用可以让函数和主函数之间产生真正的联系,在函数内部可以修改主函数中的变量。

 所以只是需要主函数中的变量值来计算,使用传值调用即可。

函数内部要修改主函数的变量的值,用传址调用。

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