NO.40十六届蓝桥杯备战|指针和动态内存管理|取地址操作符|解引用操作符|指针+-整数|void*|new|delete(C++)
内存和地址
计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是 8GB/16GB/32GB 等,那这些内存空间如何⾼效的管理
把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩为1个字节
每个内存单元也都有⼀个编号(这个编号就相当于宿舍房间的⻔牌号),有了这个内存单元的编号,CPU就可以快速找到编号对应的内存单元。
⽣活中我们把⻔牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起了新的名字叫:指针
计算机中常⻅的单位(补充):
⼀个⽐特位可以存储⼀个2进制的位1或者0
bit - ⽐特位
Byte - 字节
KB
MB
GB
TB
PB
1Byte = 8bit
1KB = 1024Byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
指针变量
取地址操作符(&)
C++中创建变量的本质是向内存申请空间
#include <iostream>
int main()
{
int a = 10;
return 0;
}
上述的代码就是创建了整型变量a,在内存中申请4个字节,⽤于存放整数10,申请到的每个
字节都有地址,上图中4个字节的地址分别是
0x0117FAD8
0x0117FAD9
0x0117FADA
0x0117FADB
操作符(&)-取地址操作符
#include <iostream>
using namespace std;
int main()
{
int a = 10;
&a;//取出a的地址
cout << &a << endl;
return 0;
}
会打印处理: 0117FAD8 &a取出的是a所占4个字节中地址较⼩的字节的地址。
虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸⽠访问到4个字节的数据也是可⾏的。
指针变量
通过取地址操作符(&)拿到的地址是⼀个数值,⽐如: 0x0117FAD8 ,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量中
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int * pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
指针变量中存放谁的地址,我们就说这个指针变量指向了谁;上⾯代码中 pa 就是存放 a 的地址,我们就是 pa 指向了 a 变量。
但是有时候⼀个指针变量创建的时候,还不知道存储谁的地址,那怎么办呢?在C++中这时候,我们会给指针变量赋值为 NULL , NULL 的值其实是 0 ,表⽰空指针,意思是没有指向任何有效的变量。
当然 0 也是作为地址编号的,这个地址是⽆法使⽤的,读写该地址会报错
int *p = NULL;
在C++11后,使⽤ nullptr 来代替了 NULL ,我们在代码中也可以直接使⽤ nullptr
如何拆解指针类型
int a = 10;
int * pa = &a;
这⾥pa左边写的是 int* , * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int) 类型的对象。
那如果有⼀个 char 类型的变量 ch , ch 的地址,要放在什么类型的指针变量中呢?
char ch = 'w';
pc = &ch; //pc 的类型怎么写呢
解引⽤操作符
C++语⾔中,只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符( * )。
#include <iostream>
using namespace std;
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
cout << a << endl;
return 0;
}
上⾯代码中第7⾏就使⽤了解引⽤操作符, *pa
的意思就是通过pa中存放的地址,找到指向的空间, *pa
其实就是 a 变量了;所以 *pa = 0
,这个操作符是把 a 改成了 0 .
这⾥是把a的修改交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活
注意:如果⼀个指针变量的值是 NULL 时,表⽰这个指针变量没有指向有效的空间,所以⼀个指针变量的值是 NULL 的时候,是不能解引⽤操作的
指针类型的意义
指针类型的意义体现在两个⽅⾯:
- 指针的解引⽤
- 指针±整数
指针的解引⽤
//代码1
#include <iostream>
using namespace std;
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
//代码2
#include <iostream>
using namespace std;
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
调试我们可以看到,代码1会将 n 的 4 个字节全部改为 0 ,但是代码2只是将 n 的第1个字节改为0 。
结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
指针±整数
#include <cstdio>
int main()
{
int n = 10;
char* pc = (char*)&n;
int * pi = &n;
printf("&n = %p\n", &n);
printf("pc = %p\n", pc);
//字符的地址使⽤cout打印会以为是字符串,所以这⾥使⽤printf来打印
printf("pc+1 = %p\n", pc + 1);
printf("pi = %p\n", pi);
printf("pi+1 = %p\n", pi + 1);
return 0;
}
char* 类型的指针变量 +1 跳过 1 个字节, int* 类型的指针变量 +1 跳过了 4 个字节。这就是指针变量的类型差异带来的变化。指针 +1 ,其实跳过 1 个指针指向的元素。指针可以 +1 ,那也可以 -1
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)
void*指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的±整数和解引⽤的运算。
#include <cstdio>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}
在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个 char* 类型的指针变量。编译器给出了⼀个报错(如下图),是因为类型不兼容。⽽使⽤ void* 类型就不会有这样的问题
#include <cstdio>
int main()
{
int a = 10;
void* pa = &a;
void* pc = &a;
*pa = 10;
*pc = 0;
return 0;
}
void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。
void test(void* p)
{
//....
}
int main()
{
int a = 10;
test(&a);
double d = 3.14;
test(&d);
return 0;
}
指针访问数组
有⼀个整型数组,10个元素,默认初始化为0,现在要将数组的内容设置为1~10,然后打印数组的内容。
我们可以使⽤数组的形式,来完成任务,也可以使⽤指针的形式来完成。
我们知道数组在内存中是连续存放的,那是只要给定⼀个起始位置,顺藤摸⽠就能到后边的其他元素了。下⾯我们来使⽤指针,给⼀个整型数组做⼀个初始化,再将数组的内容打印出来。
#include <iostream>
using namespace std;
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
for(i = 0; i < 10; i++)
{
cout << *p << " ";
p++;
}
return 0;
}
- 这⾥操作的是整型数组,我们写代码时期望以⼀个整型为单位的访问,解引⽤时访问⼀个整型,+1 跳过⼀个整型,所以我们使⽤了 int* 类型的指针,如果是其他类型的数据要选择最恰当的指针类型。
- 代码中第⼀个 for 循环,我们选择使⽤ p+i 的⽅式, p 不变, i 在不断地变化,找到数组的每个元素,第⼆个 for 循环,我们选择 p++ 的效果,让 p 不断地往后寻找元素,要体会这两种的差异。
- 我们在代码中使⽤ for 循环,通过元素个数控制循环的次数。其实指针就是地址,是⼀串编号,这个编号是有⼤⼩的,那就可以⽐较⼤⼩,这就是指针的关系运算。使⽤指针关系运算,也能完成上⾯代码。请看下⽅代码:
#include <iostream>
using namespace std;
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
while(p < &arr[10])
{
cout << *p << " ";
p++;
}
return 0;
}
动态内存管理
变量的创建会为变量申请⼀块内存空间,数组的创建其实也向内存申请⼀块连续的内存空间。
int n = 10; //向内存申请4个字节的空间
char arr1[5]; //向内存申请5个字节的空间
int arr2[5]; //向内存申请20个字节的空间
这两种⽅式,如果创建是全局的变量和数组,是在内存的静态区(数据段)申请的,如果是局部的变量和数组,是在内存的栈区申请的。不管是全局变量还是局部变量,申请和回收都是系统⾃动完成的,不需要程序员⾃⼰处理。
#include <iostream>
using namespace std;
//全局变量 - 存放在内存的静态区(数据段)
int n2 = 10;
int arr2[5];
int main()
{
//局部变量 - 存放在内存的栈区
int n1 = 10;
int arr1[5];
return 0;
}
其实C++还提供了另外⼀种⽅式,就是:动态内存管理,允许程序员在适当的时候,⾃⼰申请空间,⾃⼰释放空间,⾃主维护这块空间的⽣命周期。动态内存管理所开辟到的空间是在内存的堆区。
new/delete
C++中通过 new 和 delete 操作符进⾏动态内存管理。
- new 负责申请内存, new 操作符返回的是申请到的内存空间的起始地址,需要指针存放。
- new申请⼀个变量的空间,
new[]
申请⼀个数组的空间
- new申请⼀个变量的空间,
- delete 负责释放(回收)内存
- delete 负责释放⼀个变量的空间,
delete[]
释放⼀个数组的空间
- delete 负责释放⼀个变量的空间,
- new 和 delete 配对,
new[]
和delete[]
配对使⽤
// 动态申请⼀个int类型的空间
int* ptr1 = new int;
// 动态申请⼀个int类型的空间并初始化为10
int* ptr2 = new int(10);
// 动态申请10个int类型的空间
int* ptr3 = new int[10];
//释放内存空间
delete ptr1;
delete ptr2;
delete[] ptr3;
new 不是只能给内置类型开辟空间,也可以给⾃定义类型开辟空间
#include <iostream>
using namespace std;
int main()
{
int*p = new int;
*p = 20;
cout << *p << endl;
delete p;
int *ptr = new int[10];
for(int i = 0; i < 10; i++)
{
*(ptr + i) = i;
}
for(int i = 0; i < 10; i++)
{
cout << *(ptr + i) << " ";
}
delete[] ptr;
return 0;
}
其实数组是连续的空间, new[]
申请到的空间也是连续的,那上述代码中 ptr 指向的空间能不能使⽤数组的形式访问呢?答案是可以的,上⾯代码中第18⾏代码可以换成:
cout << ptr[i] << " ";