说明:该篇博客是博主一字一码编写的,实属不易,请尊重原创,谢谢大家!
概述
1) 内存
内存含义:
- 存储器: 计算机的组成中,用来存储程序和数据,辅助
CPU
进行运算处理的重要部分。 - 内存: 内部存贮器,暂存程序/数据——掉电丢失
SRAM、DRAM、DDR、DDR2、DDR3
。 - 外存: 外部存储器,长时间保存程序/数据—掉电不丢
ROM、ERRROM、FLASH(NAND、NOR)、硬盘、光盘
。
内存是沟通CPU
与硬盘的桥梁:
- 暂存放
CPU
中的运算数据 - 暂存与硬盘等外部存储器交换的数据
2) 物理存储器和存储地址空间
有关内存的两个概念:物理存储器 和 存储地址空间。
物理存储器:实际存在的具体存储器芯片。
- 主板上装插的内存条
- 显示卡上的显示
RAM
芯片 - 各种适配卡上的RAM芯片和
ROM
芯片
存储地址空间:对存储器编码的范围。我们在软件上常说的内存是指这一层含义。
- 编码:对每个物理存储单元(一个字节)分配一个号码
- 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写
3) 内存地址
- 将内存抽象成一个很大的一维字符数组。
- 编码就是对内存的每一个字节分配一个
32
位或64
位的编号(与32位或者64位处理器相关)。 - 这个内存编号我们称之为内存地址。
内存中的每一个数据都会分配相应的地址:
- char:占一个字节分配一个地址
- int: 占四个字节分配四个地址
- float、struct、函数、数组等
4) 指针和指针变量
指针: 指针 ===
地址 ===
编号
指针变量: 存放指针(地址)的变量
- 内存区的每一个字节都有一个编号,这就是“地址”。
- 如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)
- 指针的实质就是内存“地址”。指针就是地址,地址就是指针。
- 指针是内存单元的编号,指针变量是存放地址的变量。
- 通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样。
指针基础知识
1) 指针变量的定义和使用
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
- “
*
”操作符操作的是指针变量指向的内存空间
定义指针的三步骤:
1、 *
与符号结合代表是一个指针变量
2、 要保存谁的地址,将他的定义形式放在此处
3、 用*p
替换掉定义的变量
int a = 10;
// 1、定义*p指针变量
// 2、int a ---->
// 3、int a ----> int *p // 指针变量p
分析:
1、与*
结合代表这个一个指针变量
2、p是变量,p的类型是将变量p本身拖黑,剩下的类型就是指针变量的类型 int *
3、指针变量p用来保存什么类型数据的地址 ,将指针变量p和指针变量p最近的*
一起拖黑,剩下什么类型就保存什么类型数据的地址
int a = 10;
int *p = &a;
示例:
#include <stdio.h>
int main()
{
int a = 10;
char b = 97;
printf("%p, %p\n", &a, &b); //打印a, b的地址
//int *代表是一种数据类型,int*指针类型,p才是变量名
//定义了一个指针类型的变量,可以指向一个int类型变量的地址
int* p;
p = &a; //将a的地址赋值给变量p,p也是一个变量,值是一个内存地址编号
printf("%d\n", *p); //p指向了a的地址,*p就是a的值
char* p1 = &b;
printf("%c\n", *p1); //*p1指向了b的地址,*p1就是b的值,%c打印字符
return 0;
}
注意:&
可以取得一个变量在内存中的地址。但是,不能取寄存器变量,因为寄存器变量不在内存里,而在CPU
里面,所以是没有地址的。
2) 通过指针间接修改变量的值
#include <stdio.h>
int main() {
int a = 10;
char b = 97;
int* p = &a;
char* p1 = &b;
printf("a=%d, *p=%d\n", a, *p); // a=10, *p=10
printf("b=%c, *p1=%c\n", b, *p1); // b=a, *p1=a
// 指针变量保存谁的地址就指向了谁
*p = 100; // 在使用时,*与p结合代表,取p指针所指向那块空间的内容
*p1 = 65;
printf("a=%d, *p=%d\n", a, *p); // a=100, *p=100
printf("b=%c, *p1=%c\n", b, *p1); // b=A, *p1=A
return 0;
}
重点:星花*
与取地址&
,在给变量赋值时,等号两边的表达式类型应当要匹配一致;int* p = &a;
亦是如此。
在使用时,对一个表达式取
*
,就会对表达式减一级*
,如果对表达式取&
,就会加一级*
int a = 10;
//在使用时,对一个表达式取`*`,就会对表达式减一级`*`,如果对表达式取`&`,就会加一级`*`
// 左边表达式p为int*类型;右边的表达式a为int类型那么加上`&`,对表达式取`&`,就会加一级`*,即int *类型;左右两边类型匹配
int* p;
p = &a;
// 左边表达式p为int*类型,取`*`,则减一级`*`即int类型;右边100为int类型;左右两边类型匹配
*p = 100;
int* t;
int** g;
// 左边表达式g为int**类型;右边表达式t为int*类型,取`&`就会加一级`*`即为int**类型;左右两边类型匹配
g = &t;
// 左边表达式g为int**类型,取`*`,则减一级`*`即int*类型;右边表达式t为int*类型;左右两边类型匹配
*g = t;
3) 指针大小
- 使用
sizeof()
测量指针的大小,得到的总是:4
或8
sizeof()
测的是指针变量指向存储地址的大小- 在
32
位平台,所有的指针(地址)都是32
位(4
字节) - 在
64
位平台,所有的指针(地址)都是64
位(8
字节)
#include <stdio.h>
int main() {
// 不管什么类型的指针,大小只和系统编译器有关系
char* p1;
char** p2;
short* p3;
int* p4;
int** p5;
float* p6;
double* p7;
printf("%d\n", sizeof(p1));
printf("%d\n", sizeof(p2));
printf("%d\n", sizeof(p3));
printf("%d\n", sizeof(p4));
printf("%d\n", sizeof(p5));
printf("%d\n", sizeof(p6));
printf("%d\n", sizeof(p7));
printf("%d\n", sizeof(long*));
return 0;
}
编译器X86运行结果:
编译器X64运行结果:
因为32
位编译器内存地址编号范围是0x0000 0000 - 0xffff ffff
如此,所以我们的指针变量占4
个字节就可以存下;而64
位编译器内存地址编号范围是0x0000 0000 0000 0000 - 0xffff ffff ffff ffff
这样的编号,需要8
个字节才能存下,所以指针变量也需要8
个字节
4) 指针的宽度和步长
- 不同类型的指针变量,取指针指向的内容的宽度
- 指针的宽度 = sizeof(将指针变量与指针变量最近的
*
拖黑,剩下的类型;如char *p; sizeof(char);
占一个字节;int **p; sizeof(int*);
占4个字节) - 宽度也叫做步长
- 步长:指针加1跨过多少个字节
示例1:
#include <stdio.h>
int main(){
int num = 0x01020304; // 刚好四个字节的数据
char* p1 = (char*)# // 类型不匹配强转
short* p2 = (short*)# // 类型不匹配强转
int* p3 = #
//通过*取指针变量所指向那块空间内容时,取的内存的宽度和指针变量本身的类型有关
printf("p1=%x\n", *p1); // 04 1个字节
printf("p2=%x\n", *p2); // 0304 2个字节
printf("p3=%x\n", *p3); // 01020304 4个字节
return 0;
}
示例2:
#include <stdio.h>
int main() {
int num = 0x01020304;
char* p1 = (char*)#
short* p2 = (short*)#
int* p3 = #
printf("p1=%u\n", p1); // p1=7601028
printf("p2=%u\n", p2); // p2=7601028
printf("p3=%u\n", p3); // p3=7601028
printf("\n");
printf("p1=%u\n", p1+1); // p1=7601029 p1是char类型的指针,+1跨过1个字节
printf("p2=%u\n", p2+1); // p1=7601030 p2是short类型的指针,+1跨过2个字节
printf("p3=%u\n", p3+1); // p1=7601032 p3是int类型的指针,+1跨过4个字节
return 0;
}
5) 野指针和空指针
5.1 野指针
指针变量也是变量,是变量就可以任意赋值,不要越界即可(32
位为4
字节,64
位为8
字节),但是任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
错误示例:
int a = 100;
int* p;
p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义
int* p;
p = 0x12345678; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义
int* p;
*p = 1000; //操作野指针指向未知区域,内存出问题,err
野指针就是没有初始化的指针,指针的指向是随机的,不可以操作野指针。
正确示例:
指针p保存的地址一定是定义过的(向系统申请过的)。
int a = 100;
int* p = &a;
*p = 1000; //p指向了a的地址,*p就是a的值,给*p赋值1000,那么a的值也是1000
5.2 空指针
野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C
语言中,可以把NULL
赋值给此指针,这样就标志此指针为空指针,没有任何指针。
#include <stdio.h>
int main() {
int a = 100; // 整型变量的初始化
// 将指针的值赋值为0 即0x00000000 = NULL
int* p = NULL; // 因为p保存了0x0000的地址,这个地址是程序初始地址不可以使用的,非法
*p = 1000;
printf("%d", *p);
return 0;
}
既然不能使用,为什么还要初始化为NULL
呢,赋值为NULL
主要用于标记,来判断该指针是否被使用,避免它成为一个野指针。
#include <stdio.h>
int main() {
int a = 100; // 整型变量的初始化
// 将指针的值赋值为0 即0x00000000 = NULL
int* p = NULL; // 因为p保存了0x0000的地址,这个地址是程序初始地址不可以使用的,非法
// 如果p等于NULL,说明没有被使用,那么就可以进行赋值操作
if (p == NULL)
{
p = &a;
*p = 1000;
}
else
{
// 说明p有指向,被使用
// 养成好的习惯,每次使用完指针,就给赋值为NULL
// int* p = NULL;
}
printf("%d %d", *p, a);
return 0;
}
6) 万能指针void *
void*
万能指针可以指向任意变量的内存空间:
错误示例:
#include <stdio.h>
int main() {
int a = 10;
void* p = (void*)&a; // a为int类型,取&,加一级*,所以为int*类型,int*类型不匹配void*类型,所以要进行强转
printf("%d", *p);
return 0;
}
运行以上代码,提示错误
导致以上错误原因是:我们不知道*p
应该取多少个字节的数据
就好比定义void
类型的变量一个道理,因为编译器不知道该给此变量类型分配多大的空间;但是定义void*
类型没有问题,因为指针类型数据要么4
个字节要么8
个字节(取决于编译器)
目前程序上是知道*p
指针变量应该取哪里的地址,只是不知道应该取多少个字节数据而已,那么我们可以通过转换类型的方式去获取地址数据。(如:*p
中的p
指向a
的地址,p
的类型是void*
,你要取多少个字节数据,就转为什么类型即可;我要取4
个字节的数据,那么就将p
转为int*
类型即可)
#include <stdio.h>
int main() {
int a = 10;
//void b = 20; // error 不可以定义void类型的变量,因为编译器不知道给变量分配多大的空间
// 但是可以定义void* 类型, 因为指针类型数据要么4个字节要么8个字节(取决于编译器)
void* p = (void*)&a; // a为int类型,取&,加一级*,所以为int*类型,int*类型不匹配void*类型,所以要进行强转
//printf("%d\n", *p); //error p是void*类型,编译器不知道取几个字节的大小
// *p 中的p指向a的地址,p的类型是void* 类型,你要取4个字节数据,那么将p转为int*类型即可解决
printf("%d\n", *(int*)p);
// 同理q的类型是void* 类型,我要取2个字节的数据,那么我就将q转为short*类型即可
printf("%d", *(short*)p);
return 0;
}
7) const修饰的指针变量
const
是一个C
语言的关键字,具有着举足轻重的地位。它限定一个变量不允许被改变,产生静态作用。使用const
在一定程度上可以提高程序的安全性和可靠性。另外,在观看别人代码的时候,清晰理解const
所起的作用,对理解对方的程序也有一定帮助。
const
修饰变量a
后,不能再通过变量a
去修改a
所指向内存空间里面的内容
// const 修饰变量a
const int a = 10;
a = 100; // error 修饰变量a后,不能再通过变量a去修改a所指向空间里面的内容
但可以通过指针变量*p
去修改a
地址的内容
// const 修饰变量a
const int a = 10;
int* p = &a;
*p = 100;
const
修饰指针变量*p
后,不能再通过*p
去修改变量p
所指向a
空间里面的内容
int a = 10;
// 这里const修饰的是*,不能通过*p去修改p所指向空间的内容
const int* p = &a;
*p = 100; // error 不能通过*p去修改变量p所指向a空间里面的内容
const
修饰变量p
后,变量p
本身的值不能被更改
int a = 10;
int b = 20;
// const修饰的是变量p,p保存的地址不可以修改
int* const p = &a;
p = &b; // error 变量p本身的值不能被更改
const
修饰的是变量p
和*
,p
本身的指向不能改变,不能通过*p
去修改p
所指向空间的内容
int a = 10;
int b = 20;
// const修饰的是变量p和*,p本身的指向不能改变,不能通过*p去修改p所指向空间的内容
const int* const p = &a;
p = &b; // error 变量p本身的值不能被更改
*p = 100; // error 不能通过*p去修改变量p所指向a空间里面的内容
多级指针
C
语言允许有多级指针存在,在实际的程序中一级指针最常用,其次是二级指针。- 二级指针就是指向一个一级指针变量地址的指针。
- 三级指针基本用不着,但考试会考。
#include <stdio.h>
int main() {
int a = 10;
// *p > int a > int (*p) > int *p
int* p = &a;
// *q > int *p > int *(*q) > int **q
//如果*和&相遇,相抵消
// **q == *(*q) == *(p) == a
// **q == *(*q) == *(&a) == a
int** q = &p;
// *k > int **q > int **(*k) > int ***k
int*** k = &q;
// *符号结合,代表这个k是一个指针变量
// k是一个变量
// k的类型,将变量k拖黑,剩下的类型
// k用来保存谁的地址 将变量k和k最近的*一起拖黑,剩下什么类型
// 就保存什么类型数据的地址
printf("%d\n", *p); //10
printf("%d\n", **q); //10
printf("%d\n", ***k); //10
return 0;
}
定义多级指针保存数据的地址时,定义的指针的类型只需要比要保持的数据的类型多一级*
即可
// 定义多级指针保存数据的地址时,定义的指针的类型只需要比要保持的数据的类型多一级`*`即可
int******************* g;
int******************** f = &g;
指针和数组
1) 数组名
数组名字是数组的首元素地址,但它是一个常量:
#include <stdio.h>
int main() {
int a[10] = { 3,9,5,1,4,7,6,10,2,8 };
//a = 10; //err, 数组名只是常量,不能修改
printf("a = %p\n", a); // a = 0073F904
printf("&a[0] = %p\n", &a[0]); // &a[0] = 0073F904
return 0;
}
2) 指针操作数组元素
在没有学习指针之前,我们打印数组元素,是这样子打印的
#include <stdio.h>
int main() {
int a[10] = { 3,9,5,1,4,7,6,10,2,8 };
//a = 10; //err, 数组名只是常量,不能修改
//printf("a = %p\n", a); // a = 0073F904
//printf("&a[0] = %p\n", &a[0]); // &a[0] = 0073F904
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]); //先打印原始元素
a[i] = i + 1; // 后赋值
}
printf("\n");
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]); //打印赋值后的元素
}
return 0;
}
在学习指针后,我们可以通过指针来操作数组元素
- 指针加1,跨过一个步长 如:
int *p;
的步长为sizeof(int) = 4Byte
- 要得到内存的数据,就该先得到数据的地址
*
(地址) 得到的是地址里面的内容
#include <stdio.h>
int main() {
int a[10] = { 3,9,5,1,4,7,6,10,2,8 };
//a 数组名,首元素的地址
int* p = a; // 指针p保存的是首元素的地址
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", *(p+i)); //先打印原始元素
*(p + i) = i + 1; // 后赋值
}
printf("\n");
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]); //打印赋值后的元素
}
return 0;
}
输出结果
3 9 5 1 4 7 6 10 2 8
1 2 3 4 5 6 7 8 9 10
3) 指针加减运算
3.1 加法运算
- 指针计算不是简单的整数相加
- 如果是一个
int *
,+1
的结果是增加一个int
的大小 - 如果是一个
char *
,+1
的结果是增加一个char
的大小
#include <stdio.h>
int main()
{
int a;
int* p = &a;
printf("%d\n", p); // 9435892
p += 2;//移动了2个int
printf("%d\n", p); // 9435900
char b = 0;
char* p1 = &b;
printf("%d\n", p1); // 9435871
p1 += 2;//移动了2个char
printf("%d\n", p1); // 9435873
return 0;
}
通过改变指针指向操作数组元素:
#include <stdio.h>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
int* p = a;
for (i = 0; i < n; i++)
{
printf("%d, ", *p);
p++;
}
printf("\n");
return 0;
}
输出结果
1, 2, 3, 4, 5, 6, 7, 8, 9,
两指针相加没有意义:单个指针加法运算如上面开头那种p += 2;
没有问题;但是两个指针相加没有任何意义,比如p
指针指向第一个元素地址内容,q
指针指向最后一个元素地址内容,那么结果只是一个很大的数,无其他任务实质意义。
#include <stdio.h>
int main() {
// 整个数组的步长 = sizeof(int [10]) == sizeof(a) == 10*4 = 40;
int a[10] = { 3,9,5,1,4,7,6,10,2,8 };
int* p = a; // &a[0]
int* q = (int*)(&a + 1) - 1; // 等同于int *q = &a[9]
// 两指针相加没有意义
printf("%d\n", p + q);
return 0;
}
3.2 减法运算
示例1:通过改变指针指向操作数组元素
#include <stdio.h>
// 指针加法运算
int main()
{
int a[10] = { 3,9,5,1,4,7,6,10,2,8 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
int* p = a + n - 1; // p指向最后一个元素地址,*p则就是取最后一个元素的地址内容
for (i = 0; i < n; i++)
{
printf("%d, ", *p);
p--;
}
printf("\n");
return 0;
}
输出结果
8, 2, 10, 6, 7, 4, 1, 5, 9, 3,
示例2:两指针(类型一致)相减,得到的是中间跨过多少个元素
#include <stdio.h>
int main() {
// 两指针相减
// 整个数组的步长 = sizeof(int [10]) == sizeof(a) == 10*4 = 40;
int a[10] = { 3,9,5,1,4,7,6,10,2,8 };
int* p = a; // &a[0]
// 通过地址取数组最后一个元素,那么&a+1则表示横跨整个数组,就是41,取地址则需要转为int*,最后才能得到41对应的地址,最后地址-1就得到最后一个元素地址
int* q = (int*)(&a + 1) - 1; // 等同于int *q = &a[9]
printf("%d\n", q-p); // 9
// 验证是q-p是否跨了9个元素,直接*取q的地址内容即可
printf("%d\n", *(p+9)); // 8
return 0;
}
输出结果
9
8
4) 方括号不是数组的专属
[]
并不是数组的专属[]
实际上是*()
的缩写
示例1:
#include <stdio.h>
int main() {
// `[]`并不是数组的专属
int a;
int* p = &a;
// [] == *()
// p[0] == *(p+0) == *p
p[0] = 100;
printf("%d", a); // 100
return 0;
}
示例2:
#include <stdio.h>
int main() {
// `[]`并不是数组的专属
int a;
int* p = &a;
// [] == *()
// p[0] == *(p+0) == *p
p[0] = 100;
p[1] = 200; // error 内存污染,p[1] == *(p+1) p+1跨过一个元素,指向的是a后面的地址,
//取这块地址里面的内容不能进行操作,即使显示没有问题,但是运行编译会出错
printf("%d", a);
return 0;
}
示例3:
int main() {
// `[]`并不是数组的专属
// [] == *()
int a[10] = { 3,9,5,1,4,7,6,10,2,8 };
int* p = a;
for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++)
{
// 第一种方式
printf("%d ", a[i]);
// 第二种方式
printf("%d ", *(p+i));
// 第三种方式
printf("%d ", p[i]); // p[i] == *(p+i)
// 第四种方式
printf("%d ", *(a+i)); // a[i] == *(a+i) (a+i)首元素地址阔过第i个元素,等到该元素的地址,*取该元素地址的内容
}
return 0;
}
输出结果
3 3 3 3 9 9 9 9 5 5 5 5 1 1 1 1 4 4 4 4 7 7 7 7 6 6 6 6 10 10 10 10 2 2 2 2 8 8 8 8
5) 指针数组
整型数组,是一个数组,数组的每一个元素都是整型。
指针数组,它也是数组,数组的每一个元素都是指针。
示例1:通过指针数组保存多个变量的地址,并打印指向变量地址的内容
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
int c = 30;
// int *p1 = &a; int *p2 = &b; int *p3 = &c;
int* num[3] = { &a, &b, &c };
for (int i = 0; i < sizeof(num)/sizeof(num[0]); i++)
{
printf("%d ", *num[i]); // *num[i] []优先级高于*
}
return 0;
}
输出结果
10 20 30
示例2:定义一个指针来保存数组num首元素的地址,并通过指针变量打印出指针数组中的所有元素
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
int c = 30;
// int *p1 = &a; int *p2 = &b; int *p3 = &c;
int* num[3] = { &a, &b, &c };
// 定义一个指针用来保存数组num首元素的地址
// num首元素地址 num == &num[0]
// 定义指针的类型 int ** 首先num[0]是int *类型,要保存int *类型的地址,就需要比它多一级*
int** k = &num[0];
printf("%d\n", **k);
for (int i = 0; i < sizeof(num)/sizeof(num[0]); i++)
{
printf("%d ", **(k+i)); //这里的括号不能去除,去除后就变成了10+i了
}
return 0;
}