指针:一种复合数据类型,和数组一样基于其他类型,指针用于存储特定变量的地址。
存储:所占内存大小取决于所使用的计算机系统,和指针存储的数据类型没有关系,之所以要确定数据类型是为了方便当前存储数据的访问。
指针与变量:指针用于存储变量的地址,可以用*间接值运算符访问当前地址内存储的数据,之前我们提到,变量在声明之后程序将自动为变量分配地址,并可以使用取地址运算符(&)来获取存储变量的内存地址,指针和变量就像是一枚硬币的正反面,使用*,&来翻转就可以看到对面的值。
int* p,a = 5; //p是int类型指针,a是int变量
p = &a; //*p = a = 5
*p = 6; //a = *p = 6
- * 运算符:" *p "和 a 一样具有一样的权限,可以看作指针指向变量的同时给该变量起了一个别名。
- 程序会给指针变量自动分配存储地址,在指针地址未被初始化之前指向该内存所存储的地址,该地址的值是不确定的,可能指向任意内存。所以在指针未被初始化不要对指针指向的变量进行任何操作。
- 虽然指针存储的地址是整型,但是不能当作简单整型来操作,C语言允许整数赋值,但C++只允许指针之间的赋值。同时由于地址和底层硬件(内存)关系密切,一般为十六进制,方便内存操作。
指针的的更多操作:
指针算术:
(pt = pt +/- n):将 pt 指针向正/反方向移动 n 个长度(sizeof(*pt)个字节)。
int a = pt - pn : 得到两个指向同一数组的指针之间的距离(以长度sizeof(*pt)为单位)。
#include <iostream>
using namespace std;
int main()
{
int a = 1,b=100000;
cout << &a << " " << &b << endl;
int* pt = &a;
cout << *pt << "\t\t" << pt << endl;
pt = pt + 1;
cout << *pt << "\t\t" << pt << endl;
pt = pt - 1;
cout << *pt << "\t\t" << pt << endl;
return 0;
}
pt [ n]:可以看作将( pt +/- n )为起始地址,sizeof(*pt)为长度的内存名称标记为pt [ n]。
#include <iostream>
using namespace std;
int main()
{
int a = 5,b=8;
int* pt = &a;
cout << &a << " : " << &b << endl;
pt[0] = 6;
pt[1] = 7;
cout << pt[0] << "\t" << pt[1] << "\t" << endl;
cout << &pt[0] << " : " << &pt[1] << endl;
cout << pt << endl;
return 0;
}
#include <iostream>
using namespace std;
int main()
{
int a[2] = {1,2};
cout << a <<endl;
cout << &a[0] <<endl;
cout << &a[1] <<endl;
cout << &a[2] <<endl;
return 0;
}
1、指针算术和C++内部处理数组的方式决定了指针和数组名几乎有着一样的特性,不同的是指针是变量可以进行修改,而数组名不可以
- 数组名单独出现时解释为第一个元素的地址,但使用 &a 时,a被解释为整个数组。
- 对数组进行使用sizeof运算符时,数组名被解释为整个数组。
2、实际上编译器会将 pt [ n] 看作 *( pt + n)
动态存储
目前涉及到的变量内存分配方案(包括每个类型数据所需内存的大小)在编译的时候已经完成了,而C++的OOP编程实现了在运行阶段分配(new运算符)和释放内存(delete运算符)的功能。
new运算符:可以在内存中寻找符合存储条件的内存并返回其地址。
delete运算符:可以释放指定地址的内存。
- 这里的内存指堆或者自由存储区的内存,而一般情况下变量的内存存储在栈的内存区域里。
new运算符可以找到适合的内存地址,之前我们通过变量名来访问这片内存区域,现在得到的内存我们该如何使用呢?使用指针变量。
int* p = new int;
*p = 5;
cout << *p << ":" << p << endl;
delete p;
cout << p << endl;
cout << *p << ":" << p << endl;
运行结果:
5:00C77B98
-572662307:00C77B98
- 将获取的内存地址存储在指针变量中,同时这使得这片内存有了名称(*p),通过该名称实现对内存的访问;
- 使用后再用delete运算符释放指针p所指向的地址内存(大小由指针类型决定)。
- 释放后指针仍然存在,可以继续使用
- delete运算符只能释放new运算符分配的内存
- new和delete一般成对出现,保证内存及时释放,不然容易造成内存泄漏(存储指针内存被释放,而动态分配的内存未释放,这将造成该内存空间无法访问)
- 对于同一片内存的重复释放会造成不确定的结果
动态存储应用
之前提到变量的内存分配会在编译阶段实现(静态联编),也可以使用new运算符在运行阶段进行内存分配(动态联编),如果运行阶段再决定是否分配内存能够有效节省内存空间。动态存储节省内存的同时也增加了编程的复杂性与不安全因素,只有当有必要节省内存(有大量大型数据:数组,字符串,结构体)时使用。
- 动态分配的内存并不支持sizeof运算符
动态数组:数组创建后返回首个元素地址赋给指针,由于指针算术和C++内部处理数组的方式极其相似,可以直接将指针名当作数组名来对动态数组进行访问。
- 由于指针类型和数组首地址相同,只是一个长度为一个元素的长度,只释放第一个元素的内存显然是不够的,C++提供了delete [ ] 来释放整个数组的内存。
动态字符串:字符串常量 " xxx " 实际上并不是一个数据值,而是该常量第一个元素的地址,这也解释了为什么C风格字符串只允许在初始化的时候使用常量赋值。
- 将字符串常量复制到new的内存应该使用strcpy(x,y),strncpy(x,y,n),毕竟C风格字符串只允许在初始化的时候使用常量赋值。
动态结构体:可以使用new为已经定义好结构规范的结构类型分配空间,之前我们使用成员运算符( . )来访问结构体中的各个变量,而new的结构体内存并没有名称,为此C++提供了一个使用指针访问结构体的工具箭头成员运算符(->)。
#include <iostream>
using namespace std;
struct abc
{
int a;
char b;
};
int main()
{
abc* pt = new abc;
pt -> a = 1;
pt -> b = 'a';
cout << pt -> a << " : " << pt -> b << endl;
delete pt;
abc m = {2,'b'};
abc* pn = &m;
cout << pn -> a << " : " << pn -> b << endl;
return 0;
}
简要介绍数组的替代品
模板类vector:使用动态存储的的数组类型数据,动态存储的特性让vector具有更大的自由度,可以在运行过程中通过类的方法设置、调整数组长度,在任意位置新增数据,删除数据。
模板类array:和数组一样使用静态存储,在数组的基础上增加了同类型(数据类型,长度相同)数组相互赋值,限制使用数组名访问超范围数据等方法。
vector<typeName> sz_name; //vector<typeName> sz_name(n)
array <typeName,n> sz_name;