目录
- 内存和地址
- 指针变量和地址
- 指针变量类型的意义
- const修饰指针
- 指针运算
- 野指针
- assert断言
- 指针的使用和传址调用
1. 内存和地址
先讲一个案例:
一栋宿舍楼有100个房间,要找一个房间,但是没有编号,只能挨个去找,这样效率很低,可以给每个房间上个编号:
一楼: 101,102,103…
二楼: 201,202,203…
有了房间号,就可以快速找到
把上面的例子放到计算机中,又是什么样子?
CPU在处理数据的时候,都是放在内存中处理的,处理完放到内存去,那么内存空间怎么高效管理的?
计算机中常见的内存单位 (补充) : 一个比特位可以存储一个2进制的0或1
bit -比特位
byte -字节 1byte=8bit
KB 1KB=1024 byte
MB 1MB=1024 KB
GB 1GB=1024 MB
TB 1TB=1024 GB
PB 1PB=1024 TB
每个内存单元相当于一个宿舍,一个字节空间里能放8个比特位,相当于一个房间住8个人,每个人就是一个比特位
每个内存空间也有一个编号,有了编号cpu就可以快速找到一个内存空间
c语言中内存内存单元的编号称为地址,地址也叫指针
所以也可以理解为: 内存单元的编号地址指针
1.1 如何理解编址
首先,计算机内有很多的内存单元,而硬件单元是互相协同工作的。所谓的协同,至少相互之间能够进行数据传递
硬件和硬件之间是互相独立的,如何通信呢?用“线”连起来
cpu和内存之间也有大量的数据交互的,所以两者必须用线连起来,有控制总线、数据总线和地址总线
需要读写数据时,cpu利用控制总线发出命令,从内存中通过地址总线获取地址,32为机器有32根地址总线,每根有两种状态,0和1表示脉冲有无,32根线就能表示32个数据,构成了一个地址,总共可以表示232种含义,每一种含义都是一个地址。然后在数据总线就可以取到数据,通过数据总线传入cpu计算,计算完成存到内存
2. 指针变量和地址
2.1 取地址操作符 (&)
在c语言中,创建变量其实就是向内存申请空间
nt main()
{
int a = 10;
return 0;
}
上述代码创建了整型变量n,内存中申请了4个字节,用于存放整数10,每个字节都有地址,四个地址分别是:
0x00E4F9C0
0x00E4F9C1
0x00E4F9C2
0x00E4F9C3
如何得到a的地址呢?
int a=10;
printf("%d",&a);
利用&符号取到变量的地址,会得到4个字节中地址较小的字节的地址: 0x00E4F9C0
虽然整形变量占4个字节,我们只取到了第一个字节地址,顺腾摸瓜可以访问到4个字节的数据
2.2 指针变量和解引用操作符 (*)
2.2.1 指针变量
我们取到的地址也需要存储起来,方便后期再使用,那存在哪里呢,答案是: 指针变量中
int a=10;
int* p=&a; //取到a的地址并存放到指针变量p中
指针变量也是一种变量,是用来存放地址的
2.2.2 如何理解指针类型
p的类型是int*,如何理解呢?
*号说明是指针变量,而int说明指向的对象是整形的
如果换成char类型,怎么存储呢?
char ch='w';
char* p=&ch;
2.2.3 解引用操作符
只要拿到了变量的地址,就可以通过地址去找到对象,这里需要一个操作符叫解引用操作符 (*)
int a=10;
int* pa=&a;
*pa=0;
上面就运用了解引用的操作,通过*pa=0,访问地址存的对象,把变量修改为0
想修改变量,为什么不直接a=0,而要用指针呢?用指针操作多了一种途径,对变量的修改会更加灵活,后面会有更多运用
2.3 指针变量的大小
32为机器假设有32根地址总线,32个数字信号组合成一个序列,就是地址,有32个bit位,需要4个字节才能存储
如果指针变量是用来存放地址的,那么指针的大小就得4个字节才能存储
同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来需要8个字节的空间,指针变量的大小就是8个字节
对以下代码分别用32位和64位编译
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
结论
- 32位平台下地址是32个bit位,指针变量大小是4个字节
- 64位平台下地址是64个bit位,指针变量大小是8个字节
- 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的
3. 指针变量类型的意义
为什么要有指针变量的类型,因为都是有特殊意义的,接下来继续学习
3.1 指针的解引用
//代码1
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
//代码2
include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
}
代码1将变量n地址处4个字节全部变为0,而代码2只将第一个字节改为0
结论: 指针的类型决定了,对指针解引用的时候有多大的权限 (一次能操作几个字节)
比如char类型只能访问一个字节,int可以访问四个
3.2 指针±整数
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);
代码运行的结果:
可以看出,char类型指针每次+1跳过1个字节,int跳过四个字节
总结: 指针的类型决定了指针向前或向后走一步有多大
3.3 void* 指针
有一种特殊的指针类型是void*,可以理解为无具体类型的指针 (或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但也有局限性,不能直接进行指针的±和解引用的运算,一般用来接收指针参数,实现泛型编程
int a=10;
int* pa=&a;
char* pc=&a;
编译时会给一个警告,因为int类型的变量给一个char的指针,类型不兼容,会警告,但是用void就不会有问题
int a = 10;
void* pa = &a;
void* pc = &a;
*pa = 10;
*pc = 0;
这里可以看到,void*类型的指针可以接收不同类型的地址,但无法直接进行指针运算
4. const修饰指针
4.1 const修饰变量
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改值,如果不允许修改,怎么做?这时可以用const
int m=20;
m=20; //m是可以修改的
const int n=0;
n=20; //err,n不可以修改
上述n虽然是变量,但被const修饰,就加了限制,修改值不符合规则就报错,没办法直接修改n
const int n=0;
int* p=&n;
*p=20;
printf("n=%d",n);
可以看到通过指针间接修改了const修饰的n值,但这是不合理的,那么怎么让p拿到地址但不能修改呢?
4.2 const修饰指针变量
//const放在*号左边的情况
void test1()
{
//代码2
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//ok?
p = &m; //ok?
}
//const放在*号右边的情况
void test2{
int n = 10;
int m = 20;
int *const p = &n;
*p = 20; //ok?
p = &m; //ok?
//两边都有const的情况
void test4()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20; //ok?
p = &m; //ok?
}
}
结论: const修饰指针变量时
const如果放在号左边,修饰的是指针指向的内容,指针指向的内容不能改变,但指针本身可变
const如果放在号右边,修饰的是指针本身,保证指针本身不能改变,不可以指向别的内容,但指针指向的内容可以改变
如果两边都有,那指针本身和指向的内容都不可以改变
5. 指针运算
指针的基本运算有三种:
- 指针±整数
- 指针-指针
- 指针的关系运算
5.1 指针±整数
数组是连续存放的,只需要知道第一个元素的地址,就能找到后面的元素
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]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
}
通过指针+整数,跳到下一个数组元素的地址访问,完成数组的遍历
5.2 指针-指针
#include <stdio.h>
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
strlen函数可以计算字符串的长度,怎么计算呢?它的参数传入的是第一个字符的地址,通过字符串结束符’\0’的判断和指针的变化来确定字符串长度。这里我们也实现一个求字符串长度的函数,传入字符串的首地址,先记录字符串首地址,然后不是‘\0’时不断+指针,最后通过结束时的指针-首地址指针,就会返回其中元素的个数。
指针-指针的结果是中间元素的个数,是一个整数
5.3 指针的关系运算
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++;
}
指针可以比较大小,通过判断指针大小完成数组遍历
6. 野指针
野指针就是指针指向的位置是不可知的 (随机的,不正确的,没有明确限制的)
6.1 野指针的原因
- 指针未初始化
int* p; //指针未初始化,默认为随机值
*p=20;
- 指针越界访问
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
指针加了十一次,超过了数组的范围
3. 指针指向的空间释放
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
n是个局部变量,会被销毁,用指针p接收了n的地址又进行操作,成了野指针非法访问会导致错误
越界访问,一个程序向内存申请了哪些空间,通过指针也只能访问这些空间,超出就会报错
6.2 如何避免野指针
明确知道指针指向哪里就赋值,不知道就设初始值为NULL,NULL是一个标识符常量,值是0,0也是个地址,但这个地址是无法访问的,读写会报错
//NULL的定义
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
int n=10;
int* p1=&n; //直接明确初始化
int* p1=NULL; //设空值
避免返回局部变量的地址,防止第3个代码的问题
局部变量未初始化是随机值
全局变量和静态局部变量未初始化值默认0,因为是存储在静态区的
6.3 指针不再使用时,及时设置NULL,使用之前检查有效性
只要是NULL指针就不去访问,同时使用之前要判断指针是否为NULL
可以吧野指针想象成野狗,放任不管是危险的,设为NULL,就是把狗找棵树拴起来,暂时管理野指针
但是即使拴着直接去挑逗也是危险的,所以使用之前判断是否为NULL,看是不是被拴起来的,不是NULL再使用
int arr[10] = {1,2,3,4,5,67,7,8,9,10};
int *p = &arr[0];
for(i=0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//使用前先重新赋值
p= &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
7. assert断言
assert.h定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合就报错终止运行,这个宏被称为"断言"
assert (p!=NULL)
这样会验证p的值是否为NULL,如果符合条件不等于NULL,会继续运行,否则终止运行,并给出报错信息提示
assert()宏接受一个表达式作为参数,如果表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行。如果为假(返回值非零),assert()就报错,在标准错误stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号
使用assert有几个好处,不仅能标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert的机制。如果已经确认程序没有问题,不需要再断言,就在#include <assert.h>语句前面,定义一个宏NDEBUG
define NDEBUG
#include <assert.h>
加上NDEBUG定义,重新编译,编译器就会禁用文件中所有的assert()语句。如果程序又出问题,可以移除这条宏,再次编译,重新启用assert()语句
一般可以在Debug中使用,在换成Release版本时禁用就可以,在vs环境中,Release版本会自动优化掉,这样不会影响效率
8. 指针的使用和传址调用
8.1 strlen的模拟实现
库函数strlen功能是求字符串长度,统计字符串’\0’之前的字符个数
函数原型如下:
size_t strlen (const char* str);
参数str接收一个字符串的起始地址,然后开始统计字符串\0前的字符个数,最终返回长度
模拟实现只需要遍历字符串,不是\0,计数器+1
int my_strlen(const char * str)
{
int count = 0;
assert(str);
while(*str)
{
count++;
str++;
}
return count;
}
传入字符串其实是传的字符串的首字符地址
8.2 传值调用和传址调用
什么问题,非用指针不可呢?
例如: 写一个函数,交换两个整数的值
#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,a的地址是0x00cffdd,b的地址是0x00cffdc4,在调用时,用形参x和y接收a,b的值,但x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收了值,不过地址不一样,相当于x和y是独立的内存空间,那么函数内交换值不会影响到a和b本来的值。swap函数是吧变量本身传给了函数,这种叫传值调用
结论: 实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参,所以swap是失败的
那该怎样才能改变实参的值,可以使用指针,将a和b的地址传递给swap函数,通过访问地址修改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;
}
输出结果:
可以看到,顺利交换了,这种调用函数将变量的地址传给函数的方式叫: 传址调用
如果函数内部要修改主调函数中变量的值,就需要传址调用