壹 内存和地址
1.1 内存
计算机中常见的单位(补充):
一个比特位可以存储一个二进制的位1或者0
概念类比:
所以我们可以理解为:内存单元的编号 = 地址 = 指针
1.2 如何理解编址
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
总结:
① 内存被划分为一个个单元,一个内存的大小是一个字节。
② 每个内存单元都给一个编号,这个编号就是地址,C语言中把地址又称为“指针”。
贰 指针变量和地址
2.1 取地址操作符(&)
#include<stdio.h>
int main()
{
int a = 10;
return 0;
}
上示代码的表意是创建一个变量a,并赋值为10;深层是在内存上申请4个字节的空间,存放10
int * pa = &a;
pa 是指针变量—— 用来存放地址——地址又被称为指针,指针变量是用来存放指针的
指针——指针变量
地址——变量:存放地址
口语中所说指针一般是指针变量
2.2 解引用操作符(*)
*pa = 20; // * 是解引用操作符,*pa 等价于 a
有时直接操作不够方便,就把 a 的地址信息交给指针 pa ,用 *pa 来改变 a 的地址,看起来貌似 &(取地址) 和 *(解引用操作符) 是一对
二者关系:一来一去,拿到地址可以解引用
2.3 指针变量的大小
1. 指针变量是专门用来存放地址的,那么指针变量的大小是多少呢?
答:取决于一个地址的存放需要多大空间。
举例:32位机器上:地址线是32根,地址的二进制序列就是 32bit 位,要把这个地址存起来,需要4个字节的空间,也就是 32bit 位的空间。
所以,
32位机器上指针变量的大小都是4个字节,64位同理(8个字节)
指针变量的大小和类型是无关的,只要是指针变量,在相同的平台下,大小都是相同的。
叁 指针变量类型的意义
结论:指针的类型决定了对指针解引用的时候有多大的权限(即:一次能操作几个字节),
比如:char* 的指针解引用就只能访问一个字节,而 int* 的指针解引用一次就能访问四个字节,两个字节是 short* ;八个是 double*
3.1 指针 +/- 整数
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)
%p 是专门用来打印地址的。
3.2 void* 指针
void* 指针可以理解为无具体类型的指针(或者叫泛指针),这种类型的指针可以用来接收任意类型地址,但是也有局限性
void* 类型的指针不能直接进行指针的 +/- 整数和解引用的运算,即无法直接进行指针运算
那么 void* 类型的指针到底有什么用呢?
一般 void* 的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数可以处理多种类型的数据。
肆 const 修饰指针
const 意为常属性(即不变的属性)
#include<stdio.h>
int main()
{
int a = 10;
a = 20;
return 0;
}
上示代码的 a 值可以随意更改,但倘若如下代码使用了 const :
#include<stdio.h>
int main()
{
const int a = 10;
a = 20;
return 0;
}
编译器就会报错,所以 const 类似保护 a 的警察,a 的本质上还是自由的(还是变量),const 仅仅是在语法上做了限制,所以我们习惯上把 a 叫做常变量。
不太好的写法:
该行为其实很危险,所以我们可以写成如下形式:
这样编译器就会报错了。
const 修饰指针的时候,const 可以放在 * 的左边,也可以放在 * 的右边。
4.1 const 放在*左边
实际上,
const int* p = &a;
和
int const* p = &a;
效果是一样的,只要 const 放在 * 的左边
const int* p; 限制的是 *p,意思是:不能通过 p 来修改 p 指向空间的内容
const 放在*的左边,限制的是 *p,意思是不能通过指针变量 p 来修改p指向空间的内容,写成 *p = 20; 是错误的,但是 p 是不受限制的,写成 p = &b;是可以的。
4.2 const 放在*右边
int* const p; 限制的是p,
const 放在*的右边,限制的是变量 p ,也就是 p 变量不能被修改了,没办法再指向其它变量了,p = &b;是错误的,但是相应的 *p 就不受限制,还是可以通过 p 来修改 p 所指向对象的内容,*p = 20;是可以的
4.3 三个概念
- p里面存放的是地址(a的地址)
- p是变量,有自己的地址
- *p 是 p 指向的空间
伍 指针运算
指针的基本运算有三种:
- 指针 +/- 整数
- 指针 - 指针
- 指针的关系运算(例如比较大小)
5.1 指针 +/- 整数
详见上文 3.1指针 +/- 整数
5.2 指针 - 指针
其实就是地址减地址(看的是元素个数,而不是字节个数)
| 指针 - 指针 |(绝对值)得到的是指针和指针之间的元素个数
指针 - 指针运算的前提条件是:两个指针指向同一块空间
举例:
如上代码编译会 warning 说明了如下问题:
- 两个数组在内存中到底是什么关系,以及二者之间有没有空隙
- 当这两个指针相减的时候,二者之间的元素个数,到底按 char 算还是按 int 算是不确定的。
类比:指针?地址?——日期
5.2 指针的关系运算
其实就是指针比较大小(地址比较大小)
注:一般在使用数组时,不会把数组大小固定不变,所以一般都会计算:
int sz = sizeof(arr) / sizeof(arr[0]);
注:arr 是数组名,数组名其实是数组首元素的地址
所以 &arr[0] 等价于 arr
陆 野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不确定的、没有明确限制的)
6.1 成因
6.1.1 指针未初始化
局部变量如果不初始化,变量的值是随机的
全局变量如果不初始化(相当于静态变量),变量的值默认是0
(因为全局变量和静态变量都是放在静态区的)
6.1.2 指针越界访问
6.1.3 指针指向的空间释放
6.2 如何规避野指针
6.2.1 指针初始化
①:int a = 10;
int* p = &a; //这里明确知道 p 应该指向 a ,所以拿 a 的地址初始化
②:可能一开始不知道给指针初始化谁的地址,直接赋一个NULL
int* pa = NULL;
NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
#ifdef _cplusplus
#define NULL 0
#else
#define NULL ((void*)0) //强制类型转换0为一个空指针类型(地址值是0)
#enddif
6.2.2 小心指针越界
6.2.3 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
6.2.4 避免返回局部变量的地址
......等等
柒 assert 断言
assert.h 头文件定义了宏 assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常常被称为“断言”。
assert( p != NULL );
assert() 宏接受一个表达式作为参数,如果该表达式为真(即返回值非零),assert() 不会产生任何作用,程序继续运行。
如果该表达式为假(即返回值为零),assert()就会报错,在标准错误流 stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号
7.1 使用 assert 的好处:
① 它不仅能自动标识文件和出问题的行号
② 还有一种无需更改代码就能开启或关闭 assert() 的机制。(如果已经确定程序没有问题,不需再做断言,就在 #include<assert.h> 语句的前面,定义一个宏 NDEBUG)
#define NDEBUG
#include<assert.h>
根据需要,自由选择是否注释(关闭)或取消注释(启用)assert() 语句。
7.2 缺点
因为引用了额外的检查,增加了程序的运行时间。
一般我们在 Debug 中使用,在 Release 版本中选择禁用 assert() 就行,在VS这样的集成开发环境中,在 Release 版本中直接就优化掉了,这样在 Debug 版本中写有利于程序员排查问题,在Release 版本不影响用户使用时程序的效率
捌 指针的使用和传址调用
函数的调用:
8.1 strlen 的模拟实现
#include<string.h>
8.2 传值调用和传址调用
传值(即没有指针),传址必然会用到指针
传值调用函数时,函数的实参传给形参时,形参是实参的一份临时拷贝!
形参有自己独立的空间,对形参的修改不会影响实参!
总结:
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;
所以在未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。
如果函数内部要修改主调函数中的变量的值,就需要传址调用。