内存和地址
内存
我们知道计算机上CPU(中央处理器)在处理数据的时候, 数据是在内存中读取的,处理后的数
据也会放回内存中,内存被分为⼀个个的内存单元,每个内存单元的大小取1个字节。
计算机中常见的单位(补充): ⼀个比特位可以存储⼀个2进制的位1或者0
每个内存单元都有⼀个编号(就相当于宿舍的门牌号),CPU通过编号就能找到这个内存空间
我们把内存单元的编号也称为地址
C语言中给地址起了新的名字叫:指针 内存单元的编号==地址==指针
编址
CPU访问内存中的某个字节空间,要知道这个字节空间在内存的什么位置,所以要给内存进行编址
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的
计算机内是有很多的硬件单元,硬件与硬件之间是互相独立的,而硬件单元是要互相协同工作的,至少相互之间要能够进行数据传递,那么如何通信呢?答案很简单,用"线"连起来
而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来
今天关心一组线,叫做地址总线
简单理解,32位机器有32根地址总线, 每根线表示0,1【电脉冲有无】,就能表示2种含义
32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址
地址信息被下达给内存,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器
指针变量和地址
取地址操作符(&)
在C语言中创建变量其实就是向内存申请空间
那我们通过取地址操作符(&)拿到的地址是⼀个数值
比如:0x006FFD70,这个数值有时需要存储起来方便后期再使用,可以把它存放在指针变量中
指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址
如何拆解指针类型
指针变量名左边写的是类型的对象。
例如int* , * 是在说明是指针变量,int 是在说明指向的是整型(int)类型的变量
解引用操作符(*)
我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象,这里必须学习⼀个操作符叫解引用操作符(*)。
*pa 的意思就是通过pa中存放的地址,找到指向的空间
指针变量的大小
32根地址线产生的2进制序列当做⼀个地址,⼀个地址就是32个bit位,需要4个字节才能存储
那么存放地址的指针变的大小就是4个字节的空间。
同理64位机器,⼀个地址就是64个⼆进制位组成的指针变量的大小就是8个字节
所以
32位平台下地址是32个bit位,指针变量大小是4个字节
64位平台下地址是64个bit位,指针变量大小是8个字节
注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的
指针变量类型的意义
指针的类型决定了对指针解引用的时候有多大的权限(一次能操作几个字节)
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节
指针的类型决定了指针向前或者向后走一步有多大(距离)
指针+1,其实是跳过1个指针指向的元素,指针可以+1,那也可以-1
比如:char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节
void* 指针
在指针类型中有⼀种类型是 void * 类型,可以理解为无具体类型的指针(泛型指针)
这种指针可以用来接受任意类型地址
但是void* 类型的指针不能直接进行指针的+-整数和解引用的运算
如果将int类型的变量的地址赋值给⼀个char*类型的指针变量,编译器会因类型不兼容给出警告
而使用void*类型就不会有这样的问题。
那么void* 类型的指针到底有什么用呢?
⼀般void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,实现泛型编程的
效果,使得⼀个函数来处理多种类型的数据
const 修饰指针
const修饰变量
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量的也可以修改这个变量
但是如果我们希望一个变量不能被修改,就可以使用const进行修饰
被const修饰后,在语法上加了限制,在代码中对n进行修改,就会不符合语法规则,出现报错,致
使没法直接修改n
但是如果我们绕过这个变量,用它的指针去修改就能做到了
const修饰指针变量
const放在*的左边
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变
但是指针变量本身的内容可变
const放在*的右边
const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改
但是指针指向的内容,可以通过指针改变
指针运算
指针+-整数
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素
指针-指针
指针的关系运算
野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
1.指针未初始化
2.指针越界访问
3.指针指向的空间释放
如何规避野指针
1.指针初始化
2.小心指针越界
3.指针变量不再使用时,及时置NULL,指针使用之前检查有效性
4.避免返回局部变量的地址
assert断言
assert.h 头文件定义了宏assert()
用于在运行时确保程序符合指定条件,不符合就报错终止运行,这个宏常常被称为“断言”
assert(p != NULL);
上面代码在程序运行到这⼀行语句时,验证变量 p 是否等于NULL
如果确实不等于继续运行,否则就会终止运行,并且给出报错信息提示
NULL,程序 assert()宏接受⼀个表达式作为参数
如果该表达式为真(返回值非零), 不会产生任何作用,程序继续运行
如果该表达式为假(返回值为零),就会报错,在标准错误流stderr中写入⼀条错误信息,显示没
有通过的表达式,以及包含这个表达式的文件名和行号
assert()的好处:
能自动标识文件和出问题的行号,有⼀种无需更改代码就能开启或关闭 assert()的机制
如果确认程序没有问题,不需要再做断言,就在 #include 语句的前面,定义⼀个宏NDEBUG
然后重新编译程序,编译器就会禁用文件中所有的assert()语句
如果程序又出现问题,移除这条 #define NDEBUG指令(或者把它注释掉),再次编译,就能重
新启用assert()语句
assert()的缺点:
引入了额外的检查,增加了程序的运行时间
可以在Debug中使用,在Release版本中选择禁用,
VS这样的集成开发环境中, 在Release版本中的assert()语句,直接就是优化掉了
这样在debug版本写有利于程序员排查问题,Release版本不影响用户使用程序时的效率
指针的使用和传址调用
strlen的模拟实现
库函数strlen的功能是求字符串长度,统计的是字符串中\0 之前的字符的个数函数原型如下
size_t strlen ( const char * str );
参数str接收⼀个字符串的起始地址,然后开始统计字符串中\0之前的字符个数,最终返回长度
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是\0 字符,计数器就+1,直到
\0就停止
参考代码如下:
int my_strlen(const char * str)
{
int count = 0;
assert(str);
while(*str)
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
传值调用和传址调用
学习指针的目的是使用指针解决问题,那什么问题,非指针不可呢?
例如:写⼀个函数,交换两个整型变量的值一番思考后,我们可能写出这样的代码:
#include <stdio.h>
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;
}
在main函数内部,创建了a和b,在调用 Swap1函数时,将a和b传递给了Swap1函数,在Swap1函
数内部创建了形参x和y接收a和b的值,x和y确实接收到了a和b的值,不过x的地址和a的地址不一
样,y的地址和b的地址不⼀样,相当于x和y是独立的空间,那么在Swap1函数内部交换x和y的值,
不会影响a和b,当Swap1函数调用结束后回到main函数,a和b的没法交换
Swap1函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数
的时候就知道了,这 种叫传值调用
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实
参,所以Swap1失败了。
我们现在要解决的就是当调用Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,
直接将a和b的值交换了,那么就可以使用指针了,在main函数中将a和b的地址传递给Swap函数,
Swap函数里边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了
#include <stdio.h>
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);
Swap2(&a, &b);
printf("
交换前:
a=%d b=%d\n", a, b);
printf("
交换后:
a=%d b=%d\n", a, b);
return 0;
}
可以看到Swap2顺利完成了任务,Swap2函数是将变量的地址传递给了函数
这种函数调用方式叫:传址调用, 可以让函数和主调函数之间建立真正的联系,在函数内部可以
修改主调函数中的变量
所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用
如果函数内部要修改主调函数中的变量的值,就需要传址调用