1. 内存和地址
我们知道计算机上的CPU(中央处理器)在处理信息的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。在购买电脑时,电脑内存有8G/16G/32G等等,那么这些空间又是如何高效管理的呢?
其实,计算机会把内存空间划分为一个个的内存单元,每个内存单元的大小取1个字节。
计算机中常见的单位,如下:
1个比特位可以存储一个2进制的位1或者0;
1.1 内存的布局
- 栈区内存的使用习惯是从高地址向低地址使用,所以变量i的地址是较大的。arr数组的地址整体是小于i的地址。
- 数组在内存中存放是:随着下标的增长,地址由低到高。
因此,随着数组下标的增长,往后越界就有可能覆盖到 i,就有可能造成死循环。
注意:栈区默认是先使用高地址再使用低地址,但具体还是取决于编译器的实现。
因此,我们可以理解为:
地址 == 指针
1.2 如何理解编址
首先,必须要理解,计算机内是有很多的硬件单元,而硬件单元是相互协调工作的。所谓协调工作,就是相互之间必须进行数据传递。
但是硬件和硬件之间是相互独立的,因此就是通过“线”来实现通信。而CPU和内存之间也有大量的数据交互,所以两者也必须用线连接起来。今天,我们就来了解一组线,叫地址总线。
以上,我们可以简单理解,32位机器有32跟地址总线,每根线只有两态,表示0,1(含义为:电脉冲有无),那么一根线就有两种含义,2根线就有4种含义,依此类推。32根线,就有2^32种含义,每一种含义就代表了一个地址。
2.指针变量和地址
2.1 取地址操作符
比如,以上的代码就是创建了整型变量a,内存中申请了4个字节,用于存放整数10,其中的每个字节都有地址。其中,&(取地址符)就能获得a的地址。虽然整型变量占用4个字节,我们只要知道了第一个字节地址,顺藤摸瓜就能访问到4个字节的数据也是可以的。
2.2 指针变量和解引用操作符
指针变量:我们通过取地址操作符获得的地址是一个数值,比如:0x006FFD70,这个数值有时候也需要存储起来,因此,我们将这样的地址存放在:指针变量中。
int main()
{
int a = 10;
int* pa = &a;
return 0;
}
指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
指针变量的说明:
pa左边写的是int * ,*说明pa是指针变量,而前面的int说明pa指向的是int类型的多催下。
如果是一个char类型的变量ch,取ch的地址就该如下表示:
int main()
{
char ch = 'w';
char* pc = &ch;
return 0;
}
解引用操作符:
int main()
{
int a = 10;
int* pa = &a;
*pa = 0;
return 0;
}
*pa就是通过pa中存放的地址,找到指向的空间,*pa的意思就是a变量,所以*pa=0就是把a改成了0。
2.3 指针变量的大小
指针变量的大小取决于地址的大小。
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}
注意:sizeof返回的是无符号整型,因此用“%zd”。
X86环境下输出结果
X64环境输出结果
结论:
- 32位平台下地址是32个bit位,指针变量大小是4个字节
- 64位平台下地址是64个bit位,指针变量大小是8个字节
指针变量大小和类型是无关的,只要是指针类型的变量,在相同的平台下,大小都是相同点
3. 指针类型的意义
include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
通过调试,我们可以看到,代码1会将n的4个字节全部改为0;代码2只能将n的第一个字节改为0。
比如:char*的指针解引用就只能访问一个字节,而int *的指针的解引用就只能访问4个字节。
3.1 指针+-整数
我们可以看出,char *类型的指针变量+1跳过1个字节,int *类型的指针变量+1跳过了4个字节。
指针+1,其实跳过1个指针指向的元素。
结论:指针的类型决定了指针向前或者向后走一步有多大距离。
3.2 viod*指针
在指针类型中有一种特殊的类型是void*类型的,可以理解为无具体类型的指针,这类指针可以用来接受任意类型的地址。但是也有局限性,void *类型的指针不能直接进行指针的+-整数和解引用的运算。
4.const 修饰指针
在前面我们学过通过const来修饰变量,使变量不能被修改。
但是在学过指针的解引用操作后,我们就可以绕过变量,使用变量的地址来对变量的值进行修改。
4.1 const修饰指针变量
一般来说const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不一样的。
先说结论:
- const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
- const int * p = &n;
- const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。
- int * const p = &n;
5.指针的运算
指针的基本运算有三种,分别是:
- 指针+-整数
- 指针-指针
- 指针的关系运算
5.1 指针+-整数
因为数组在内存中连续存放的,只要知道第一个元素的地址,就可以顺藤摸瓜找到后面的元素。
- p指向整型元素a[0],p+1跳过4个字节,也就是一个整型,所以p+1指向整型元素a[1]。
- 同理,*(p+1)得到arr[1],按照指针的方法就可以打印数组的所有的元素。
- 其实,arr[0]本质就是*(p+0),arr[i] = *(p+i)。
- 此外,在这里arr的数组名就是首元素的地址,也就是arr[0],也可以这么写arr[0]等价于*(arr + 0);arr[i] 等价于 *(arr+i)
5.2 指针-指针
- 大指针-小指针 得到的就是指针之间元素的个数,仅限于他们是同一块空间,另外,小指针-大指针就是负数。
5.3 指针的关系运算
指针还能比较大小,指针本质是地址,地址以16进制显示出来,所以本质就是比较两个数的大小
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
6. 野指针
概念:野指针就是指指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1 野指针成因
野指针成因如下:
6.1.1指针未初始化
include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
6.1.2指针访问越界
include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
6.1.3指针指向的空间释放
#include <stdio.h>
int * test()
{
int n = 100;
return &n;
}
int main()
{
int * p = test();
printf("%d\n",*p);
return 0;
}
6.2如何规避野指针
6.2.1 野指针初始化
如果不知道指针指向哪里,可以赋值给NULL。
#include <stdio.h>
int main()
{
int *p = NULL;
return 0;
}
6.2.2 小心指针越界
一个程序向内存申请了哪些空间,通过指针也就访问哪些空间,不能超出访问范围,超出了就是越界访问。
6.2.3 指针变量不再使用时,即使置NULL,指针使用之前检查有效性
7. assert断言
使用assert前,应该包含头文件assert.h。用于在运行时确保程序符合指定条件,如果不符合,就会报错。经常被称为“断言”
assert(p != NULL)
关于assert()的几个好处:它不仅能自动标识文件和出问题的行数,而且当已经确定程序没有问题,不需要再做出断言,就在头文件前定义一个宏NDEBUG
#define NDEBUG
#include <assert.h>
8. 指针的使用和传址调用
库函数strlen的功能是求字符串长度,统计的是字符串中\0之前的字符的个数。
函数的原型如下:
size_t strlen(const char *str);
参数str接收一个字符串的起始地址,然后开始统计字符串中\0之前的字符个数,最终返回长度。
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是\0字符,计数器就+1,这样直到\0就停止。
8.1传址和传值调用
学习指针的目的是用指针解决问题,那什么问题,非用指针不可呢?
例如:写一个函数,交换两个函数的值:
我们可能写出以下代码:
我们发现运行之后的并没有发生交换的效果。那原因是什么呢?
原来,我们发现在调用swap函数的时候,将a和b的值传递给了x和y,在swap函数内部创建了形参x和y接收了a和b的值,但是x和y的地址与a和b的地址并不一样,相当于x和y是独立的空间。那么在swap函数内部交换x、y的值,自然不会影响a和b。
swap函数在使用的时候,是把自身的变量直接传递给了函数,这种传递称之为传值调用。
结论:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改并不影响实参。
那么要怎么办呢?
我们就可以考虑指针的作用了
这样将变量的地址传给了函数,这种函数调用方式为:传址调用。
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。
如果函数内部要修改主调函数中的变量的值,就需要传址调用。