文章目录
- 指针
- 1、指针概述
- 1.1 存储器和存储地址空间
- 1.2 内存地址
- 1.3 指针和指针变量
- 2、声明和初始化指针变量
- 2.1 指针变量的声明
- 2.2 指针变量的初始化
- 3、使用指针变量
- 3.1 解除引用
- 3.2 野指针和空指针
- 4、指针的宽度和跨度
- 4.1 自身类型和指向类型
- 4.2 指针变量所取内容的宽度
- 4.3 指针变量加1的跨度
- 5、指针数组
- 6、二级指针
- 7、数组的地址
- 8、`new` 和 `delete` 运算符
- 8.1 使用 `new` 分配内存
- 8.2 使用 `delete` 释放内存
- 9、动态数组的创建
- 9.1 使用 `new` 创建动态数组
- 9.2 使用 `new` 和 `delete` 的规则
- 10、动态结构体的创建
- C++ 内存管理
- 1、C++ 的内存划分
- 2、C++ 管理数据内存的方式
- 3、内存泄漏
- 类型别名
- 字符函数库 —— `cctype`
指针
1、指针概述
1.1 存储器和存储地址空间
- 存储器:计算机的组成中,用来存储程序和数据,辅助 CPU 进行运算处理的重要部分。
- 内存:内部存储器,暂存程序或数据,掉电丢失。如
SRAM
、DRAM
、DDR3
、DDR4
; - 外存:外部存储器,长时间保存程序或数据,掉电不丢失。
ROM
、FLASH
、硬盘、光盘;
- 内存:内部存储器,暂存程序或数据,掉电丢失。如
- 内存是沟通 CPU 和 硬盘的桥梁:
- 暂时存放 CPU 中的运算数据;
- 暂时存放与硬盘等外部存储器交换的数据;
- 存储地址空间:对存储器编码的范围。
- 编码:对每个物理存储单元(一个字节)分配一个号码;
- 寻址:根据分配的号码找到相应的存储单元,完成数据的读写;
1.2 内存地址
- 将内存抽象为一个很大的一维字符数组,通过编码为内存的每一个字节分配一个32位或64位的编号,这个内存编号就被称为内存地址(唯一);
- 内存中的数据会被分配不同的地址个数。例如
char
占一个字节分配一个地址,int
占四个字节分配四个地址… - 对变量应用地址运算符
&
可以获取其在内存中的地址。例如对于变量int num
,则&num
就是它的地址;
1.3 指针和指针变量
- 指针:和
int
、double
等基础数据类型类似,是一种独立的数据类型,不同的前者存储的是实在的数据,但是指针这种类型存储的是指向这些数据的内存地址; - 指针变量:本质就是变量,只是该变量存储的是内存地址,而不是普通的数据。不同类型的指针变量所占用的存储单元长度是相同的;
- 为什么要使用指针类型来保存地址值:
- 编译时类型检查;
- 指明一个内存地址所保存的二进制数据该怎么解释;
2、声明和初始化指针变量
2.1 指针变量的声明
数据类型 * 变量名; int * num;
数据类型 *变量名; int *num;
数据类型* 变量名; int* num;
这里需要强调的是,数据类型*
才是指针变量的数据类型,如 int*
、double*
等等。而对于在哪里添加空格,对于编译器来说没有任何区别。
*
前面的类型是什么,*num
中就存储的是什么类型的数据。如 int* p
,说明 *p
获取的是 int
型数据,但是存储地址的变量 p
的长度都是相同的,它取决于计算机系统。*p
这种形式的表示方法的意思之后会介绍。
注意:对每一个指针变量名,都需要使用一个 *
:
int* p1, p2; // 声明创建一个指针变量p1和一个int类型变量p2
int *p1, *p2; // 声明创建一个指针变量p1和一个指针变量p2
2.2 指针变量的初始化
-
在声明语句中初始化指针变量:
int num = 5; int *pt_num = #
-
声明指针变量后对其进行初始化:
double d = 2.0; double *pt_d; pt_d = &d;
3、使用指针变量
3.1 解除引用
-
*
运算符称为间接值(indirect value)或解除引用运算符,将其应用于指针变量,可以得到该地址处存储的值; -
即假如
ptr
是一个指针变量,则ptr
存储的是一个地址,而*ptr
表示存储在该地址处的值; -
一定要在对指针变量应用解除引用之前,将指针变量初始化为一个确定的、适当的地址。因为在 C++ 中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存:
long *ptr; *ptr = 22332223; // 不能这样赋值!!
若分配给
ptr
的地址中有重要的数据、甚至是系统值,此时给里面赋值,很可能就造成错误! -
不能简单的将整数赋值给指针变量,指针不是整型:
// int *ptr = 0xN8000000; // 错误!! int *ptr = (int *)0xB8000000;
3.2 野指针和空指针
- 任意数值赋值给指针变量没有意义,这样的指针变量就是野指针,野指针指向的区域是未知的;
- 野指针不会直接引发错误,操作野指针指向的内存区域可能会出问题;
- 在 C++ 中,值为
0
的指针变量被称为空指针(null pointer)。空指针不会指向有效的数据; - 在 C++ 中,空指针的值可以用 0、NULL、nullptr 来展示;
- C++ 提供了检测并处理内存分配失败的工具,后面再讨论;
4、指针的宽度和跨度
4.1 自身类型和指向类型
- 指针变量的自身类型:去掉变量名,剩余的部分就是指针变量的自身类型,例如
int *
或int **
; - 指针变量的指向类型:去掉变量名和离它最近的一个
*
,剩余的部分就是指针变量指向的类型,例如int
或int *
;
4.2 指针变量所取内容的宽度
指针变量所取内容的宽度是由指针变量所指向的类型长度决定的。
例如 int *p1 = (int *)0x01020304;
,p1
指向 int
类型,所取内容宽度为 4
;short *p2 = (short *)0x01020304;
,p2
指向 short
类型,所取内容宽度为 2
。
4.3 指针变量加1的跨度
指针变量加1的跨度是由指针变量所指向的类型的大小决定。
例如,int
类型所占的字节数为4,那么它的指针变量加1,就会跨越4个地址。
5、指针数组
指针数组的本质是数组,数组中的每个元素都是指针类型。
int num1 = 1;
int num2 = 2;
int num3 = 3;
int num4 = 4;
int *arr[4];
arr[0] = &num1;
arr[1] = &num2;
arr[2] = &num3;
arr[3] = &num4;
// 另一种赋值
int *arr[4] = {&num1, &num2, &num3, &num4};
6、二级指针
如果一个指针指向的是另外一个指针,称它为二级指针,或者指向指针的指针。
int num = 5; // 内存中一块空间存储了5
int *p = # // p存储了指向这块空间的地址,*p获取的是num的数值5
int **q = &p; // q存储了指向p的内存地址,*q表示p中存储的地址值,**q获取的是num的值5
7、数组的地址
一个数组的数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址。
short tell[10];
cout << tell << endl; // 0x61fe00;展示 &tell[0] 的地址
cout << (tell + 1) << endl; // 0x61fe02;加了2
cout << &tell << endl; // 0x61fe00;展示整个数组的地址
cout << (&tell + 1) << endl; // 0x61fe14;因为十六进制,所以加20为0x14
cout << sizeof(tell) << endl; // 20;将 sizeof 用于数组名,返回整个数组的长度(单位为字节)
虽然 tell
和 &tell
的两个地址在结果上是一样的,但从概念上 &tell[0](即 tell)
是一个 2 字节内存块的地址,而 &tell
是一个 20 字节的内存块地址。因此 tell+1
将地址值加2,而 &tell + 1
将地址加20。
具体的 &tell
是怎么来的呢?语句如下:
short (*pas)[10] = &tell;
8、new
和 delete
运算符
8.1 使用 new
分配内存
-
C++ 中可以使用
new
运算符在运行阶段分配内存; -
动态分配内存的格式:
typeName *pointer = new typeName; // 例如 int *p = new int;
-
new
运算符返回该内存的地址; -
数据对象指的是为数据项分配的内存块;
-
动态分配内存使程序在管理内存方面有更大的控制权;
-
new
分配的内存块通常与常规变量声明分配的内存块不同。变量的值都存储在栈内存(stack)区域中;而new
运算符从堆(heap)或自由存储区(free store)的内存区域分配内存,所以最后一定要记得释放内存;
8.2 使用 delete
释放内存
-
delete
运算符可以在使用完内存后,将内存归还给内存池。归还或释放的内存可供程序的其他部分使用; -
使用
delete
时,后面要加上指向内存块的指针变量(内存块是由new
分配):int *p = new int; ... delete p;
注意:
- 一定要配对使用
new
和delete
,斗则会发生内存泄漏(memory leak); - 不要释放已经释放的内存块;
- 不能使用
delete
来释放声明变量所获得的内存;
9、动态数组的创建
9.1 使用 new
创建动态数组
-
通常对于大型数据(如数组、字符串和结构),应使用
new
来分配; -
创建动态数组需要将数组的元素类型和元素个数数目告诉
new
,格式如下:typeName *pointer = new typeName[num]; // 例如 int *p = new int[10];
-
new
运算符返回第一个元素的地址; -
通过指针变量
pointer
来操作数组; -
对于使用
new
创建的数组,应使用delete []
来释放:delete [] p;
9.2 使用 new
和 delete
的规则
- 不要使用
delete
来释放不是new
分配的内存; - 不要使用
delete
释放同一个内存块两次; - 如果使用
new []
为数组分配内存,则应使用delete []
来释放; - 如果使用
new
为一个实体分配内存,则应使用delete
来释放; - 对空指针应用
delete
是安全的(不会有任何事情发生);
10、动态结构体的创建
将 new
用于结构体由两步组成:创建动态结构体和访问其成员。
-
创建动态结构体的方式跟普通类型完全相同,格式如下:
typeName *pointer = new typeName; // 例如: struct student { int id; string name; }; student *stu = new student;
-
访问动态结构体的成员:
(*stu).id; // 或者 stu->id;
C++ 内存管理
1、C++ 的内存划分
- 栈区:由编译器自动分配与释放,存放为运行时的函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈;
- 堆区:一般由程序员自动分配,如果程序员没有释放,程序结束时可能由系统回收;
- 全局区(静态区):存放全局变量、静态数据。程序结束后由系统释放;
- 常量区(文字常量区):存放常量值,如字符串常量。不允许修改,程序结束后由系统释放;
- 代码区:存放函数体(类成员函数和全局区)的二进制代码;
2、C++ 管理数据内存的方式
C++ 根据用于分配内存的方法,有 3 种管理数据内存的方式:自动存储、静态存储、动态存储。这 3 种方式分配的数据对象存在时间的长短方面各不相同。
-
自动存储:在函数内部定义的常规变量使用自动存储空间,被称为自动变量。它们在所属的函数被调用时自动产生,在该函数结束时自动消亡。自动变量通常存储在栈内存中。
-
静态存储:静态存储是整个程序执行期间都存在的存储方式。使变量称为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字
static
:static int num = 10;
-
动态存储:
new
和delete
运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在 C++ 种被称为自由存储空间或堆。该内存池跟用于静态变量和自动变量的内存是分开的。在栈中,自动添加和删除机制使得占用的内存总是连续的,但new
和delete
的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更困难。
3、内存泄漏
如果使用 new
运算符在自由存储空间(或堆)上创建变量后,没有调用 delete
,即使指针变量的内存被释放,但在自由存储空间上动态分配的内存也将继续存在。因为指向这些内存的指针无效,这样将无法访问自由存储空间中的这些内存,从而导致内存泄漏。
被泄露的内存在程序整个生命周期内都不可使用,因为这些内存被分配出去但无法收回。
内存泄漏可能会非常严重,以致于应用程序可用的内存被耗尽,导致程序崩溃。要避免内存泄漏,最好是养成同时使用 new
和 delete
运算符的习惯,在自由存储空间上动态分配内存,随后便释放它。
类型别名
C++ 为类型建立别名的方式有两种,一种是使用预处理器,另一种是使用 C++ 的关键字 typedef
来创建别名。
-
使用预处理器
#define BYTE char
预处理器在编译阶段将所有的
char
替换成BYTE
。 -
使用关键字
typedef
:typedef typeName aliasName; // 为 typeName 起了个别名 aliasName // 例如: typedef char byte; typedef char* byte_pointer;
由于预处理器的局限,例如 #define FLOAT_POINTER float*
,将其作用在 FLOAT_POINTER pa, pb;
时,真实情况是 float *pa, pb;
。它只会将 pa
定义成指针变量。
但是 typedef
方法不会出现上面的问题,它能够处理更复杂的类型别名。
注意:typedef
不会创建新的类型,而是为已有的类型建议一个别名!
字符函数库 —— cctype
C++ 从 C 语言种继承了一个与字符相关的、非常方便的函数软件包,它可以简化诸如确定字符是否为大写字母、数字、标点符号等工作。在 C 语言中的老式风格为 ctype.h
。
函数名称 | 返回值 |
---|---|
isalnum() | 如果字符是字母或数字,返回true,否则false |
isalpha() | 如果字符是字母,返回true |
iscntrl() | 如果字符是控制字符,返回true |
isdigit() | 如果字符是数字(0~9),返回true |
isgraph() | 如果字符是除空格之外的打印字符,返回true |
islower() | 如果字符是小写字母,返回true |
isprint() | 如果字符是打印字符(包括空格),返回true |
ispunct() | 如果字符是标点符号,返回true |
isspace() | 如果字符是标准空白字符,如空格、进纸、换行符、回车、水平制表符、垂直制表符,返回true |
isupper() | 如果字符是大写字母,返回true |
isxdigit() | 如果字符是十六进制数字(0~9、a~f、A~F),返回true |
tolower() | 如果字符为大写,返回其小写,否则返回本身 |
toupper() | 如果字符为小写,返回其大写,否则返回本身 |