指针的使用总结
指针的概念
- 内存地址
内存是一个线性的字节数组,一个字节由 8 个二进制位组成,每个字节都有唯一的编号,这个编号就是内存地址,代表一个字节的的存储空间。
- 指针的概念
指针是“指向”另外一种类型的复合类型,可以实现对其他对象的间接访问,指针的本质就是数据在内存中的地址,地址即指针。
指针变量
-
概念
指针变量就是用来保存对象地址的变量。如果指针 ptr 保存了 num 的地址,就是说 ptr 指向了 num,也就是 ptr 指向了 num 所在的内存块。 -
定义指针变量
定义指针类型的方法就是将声明符写成 *d 的形式,其中 d 是变量名。在变量名前加 * ,这个变量就变成了对应变量类型的指针变量,必要时要加( ) 来避免优先级的问题。int *p; // 声明一个 int 类型的指针 p char *p // 声明一个 char 类型的指针 p int *arr[10] // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向 int 类型对象的指针 int (*arr)[10] // 声明一个数组指针,该指针指向一个 int 类型的一维数组 int **p; // 声明一个指针 p ,该指针指向一个 int 类型的指针
-
获取对象地址
声明一个指针变量并不会自动分配任何内存,在对指针进行间接访问之前,指针必须进行初始化。指针存放了某个对象的地址,要想获取该地址,需要使用取地址符(&)。int x = 1; int *p = &x; // 指针 p 被初始化,指向变量 x ,其中取地址符 & 用于产生操作数内存地址
指针的类型必须和它所指向的对象的类型严格匹配。
-
特殊的指针
①.空指针
空指针是一个特殊的指针变量,表示不指向任何对象,而未初始化的指针则可能执行任何地方。可以通过给一个指针赋一个零值来生成一个 NULL 指针。int *p1 = NULL; int *p2 = 0; int *p3 = nullptr;
可以看到指针指向内存地址0。在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是为操作系统保留的。但是,内存地址 0 有一个特别重要的意义,它表明改指针不指向一个可访问的内存位置。
②.void* 指针
void是一种特殊的指针类型,可以用来存放任意对象的地址,但我们对该地址中存储的是什么类型的对象并不了解。
double obj = 3.14, *pd = &obj;
void *pv = &obj;//obj 可以是任意类型的对象
pv = pd;//pv 可以存放任意类型的指针
由于void
是空类型,只保存了指针的值,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。
6. 指针值
指针的值(即地址)应属于下列 4 种状态之一:
①.指向一个对象
②.指向紧邻对象所占空间的下一个位置
③.空指针,没有指向任何对象
④.无效指针,即上述情况之外的其他值
试图拷贝或以其他方式访问无效指针的值都将引发错误;在通过之前访问对象之前必须确保该指针指向了具体对象。
指针的操作
- 解引用
如果指针指向了一个对象,则允许使用解引用符(*)来访问对象。对指针解引用的就是从指针指向的内存块中取出这个内存数据。int ival = 42; int *p = &ival; cout << *p;//输出 42 *p = 0;//通过指针修改指向的内存数据 count << *p;//输出 0
- 指针的运算
①.指针与整数加减
可以对指针变量 p 进行 p++、p–、p + i 等操作,所得结果也是一个指针,只是指针所指向的内存地址相比于 p 所指的内存地址前进或者后退了 i 个操作数,p+i 表示的实际位置地址值是(p)+i * (指向对象的数据类型长度)。
②.指针相减
只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度int main(){ int a[10] = {1,2,3,4,5,6,7,8,9,0}; int sub; int *p1 = &a[2]; int *p2 = &a[8]; sub = p2-p1; printf("%d\n",sub); // 输出结果为 6 return 0; }
使用指针的好处
①.指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效;
②.在数据传递时,如果数据块较大,可以使用指针传递地址而不是实际数据,即提高传输速度,又节省大量内存;
指针和数组
-
概述
①.数组:数组在内存中是连续的一块内存空间,数组的大小固定不变,其元素个数属于数组类型的一部分;数组不允许拷贝和赋值,只能通过下标访问或者修改对象。
②.指针:指针变量类似于一个地址箱,让其初始化为某个数组元素的地址,以该地址值为基准,通过向前或向后改变地址箱中的地址值,即可让该指针变量指向不同的数组元素,从而达到通过指针变量便可以方便地访问数组中各元素的目的。 -
一维数组和指针
①.数组名称指代首元素地址
数组的名称是一个地址常量,其值是数组首元素的地址。string nums[] = {"one","two","three"};// nums 的元素是 string 对象 string *p = &nums[0];// p 指向 nums 的第一个元素 string *p2 = nums;// 等价于 p2 = &nums[0]
②.通过指针访问数组
int main() { int a[5] = { 1,2,3,4,5 }; int *p = = a;//p 指向 a[0] //方法一: for (int i = 0 ; i < 5; i++) { printf("%d\t", *(p+ i)); } //方法二: for ( p = a;p < a+5;p++ ) { printf("%d\t", *p ); } return 0; // p[i] 和 a[i] 等价、*(a+i) 和 *(p+i) 等价,均表示 a[i] // a 是常量地址,不可以执行 a++ 操作 }
③.数组下标和指针
数组的名字就是指向数组首元素的指针,对数组执行下标运算本质就是指向数组元素的指针执行下标运算。int a[5] = {1,2,3,4,5}; int *p = &a[2];// p指向索引为2的元素 int j = p[1];// p[1] 等价于 *(p+1),即 a[3] int k = p[-2];// p[-2] 即 a[0]
数组的下标运算和标准库中 vector 及 string 的下标运算不同,标准库中的下标必须是无符号类型,而数组无此要求。
④.sizeof 运算- sizeof(数组名):返回数组所有元素占有的内存空间字节数
- sizeof(指针):返回计算机系统的地址字节数,无论指针的类型是什么,如果是 32 位系统返回 4,64 位系统返回 8
- 数组大小:sizeof(数组名)/ sizeof(数据类型)
⑤.& 运算 - 数组名 a 代表数组的首地址,也是首元素的地址,a+1 就是数组下一个元素的地址
- &a 代表数组的地址,其值和 a 相同,&a+1 就是整个数组的下一个地址
int a[5] = {1,2,3,4,5}; int (*p)[5] = &a;// &a 是整个数组的地址
-
二维数组和指针
①.二维数组
二维数组的本质就是数组的数组;第一个纬度(行)表示数组本身的大小,第二个纬度(列)表示其元素的大小。
②.二维数组和指针
二维数组名就是指向第一个内层数组的指针;int ia[3][4];//大小为3的数组,每个元素是大小为4的整型数组 int (*p)[4] = ia;// p指向含有4个整型数据的数组 // ia[i] 表示第 i 行的首地址 // ia[i][j] 等价于 *(*(a+i)+j) 等价于 *(a[i]+j) 等价于 *&a[i][j]
-
数组指针和指针数组
①.数组指针:数组指针是一个指针,它指向一个数组
②.指针数据:指针数据是一个数组,数组中的每个元素都是指针int *p[10]; // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向int类型的指针 int (*p)[10]; // 声明一个数组指针 p ,该指针指向一个数组
指针和字符串
- 基本概念
①.字符串字面值:用双引号引起来的常量字符串,如 “C++”;
②.C 风格字符串:以空字符 null 结尾的字符数组,如:const char * p = “C++”;
③.string s:s 是标准模版库中 string 类型的对象;
④.char *a:a 是一个指向 char 变量的指针;
⑤.char b[]:b 是一个指针常量,指向字符数组的首位置;
⑥.char *c[]:c 数组中的元素是指针,c = &c[0] ;
⑦.char **d:d 是一个二级指针。 - 字符串常量
字符串常量的本质就是个地址,即指向字符串中第一个字符的指常量指针。
因为字符串常量就是个指针,因此可以对其进行解引用、下标运算、和整数加减的操作。 - 字符指针
字符串常量的本质就是一个地址,所以可以将其赋值给一个字符指针变量。const char *p = "Hello";// p 为指向字符串首位置的常量指针
4. 字符数组
字符数组是若干个字符变量的集合,数组中的每个字符都是变量;数组的空间应该 >= 数组长度+1,多余空间用 ‘\0’ 填充。
char str[] = "Hello";// str 是指向数组首位置的常量指针
- char a 和 char b[10]
①.相同
a 保存了字符串的首地址,b 也代表数组的首元素的地址;
②.不同
当定义 chara 时,是个指针变量,只占4个字节,用来保存一个字符串首地址,当定义 char b[10] 时,会给数组分配10个连续的内存单元,每个单元类型均为字符;a 是一个指针变量,其值可以更改,b 是一个指针常量,其值不可用更改。
③.转换
通过字符指针变量可以访问所指字符数组中保存的串,不仅可以读取该数组中保存的字符串,还可以修改该串的内容。
指针和函数
-
指针形参
函数的形参如果不是引用类型,那么就会将实参的值拷贝后赋值给形参,即函数获得的是参数值的一份拷贝;在数据量很大时复制和传输实参的副本可能浪费较多的空间和时间,此时可以使用传指针调用。
指针参数不会复制要处理数据的副本,使得被调函数能够访问和修改主调函数中对象的值。
-
函数指针
①.函数的类型
函数的类型是由它的返回值类型和形参类型共同决定的,与函数名无关。bool lengthCompare(const string &, const string &); // 函数类型为 bool(const string &, const string &)
②.函数指针
函数指针指向的是函数而非对象。函数像其他变量一样,在内存中也占用一块连续的空间,把该空间的起始地址称为函数指针。
函数名就是该空间的入口地址,故函数名是常量指针;通过指针变量就可以找到并调用该函数。
③.函数指针的定义
申明一个指向函数的指针,只需要用指针替换函数名称即可。int function(const string &, const string &);//定义了一个函数原型,函数名为 function,返回值类型为 int int *function(const string &, const string &);//定义了一个指针函数原型,函数名为 function,返回值类型为 int* int (*function)(const string &, const string &);//定义了一个函数指针变量,变量名为 function,指向的函数返回值类型为 int
④.函数指针赋值
当函数名作为一个值使用时,该函数自动地转换成指针;可以给函数指针赋 0 表示不执行任何函数。int *pf(const string &, const string &); int function(const string &, const string &); pf = function; // pf 指向名为 function 的函数 pf = &function; // 等价赋值语句,& 是可选的
⑤.通过函数指针调用函数
可以直接使用指向函数的指针调用该函数,无须提前解引用指针
int *pf(const string &, const string &); int function(const string &, const string &); pf = function; // pf 指向名为 function 的函数 int a = function("aaa","bbb");// 函数名调用 int b = pf("aaa","bbb"); // 通过指针调用 int c = (*pf)("aaa","bbb"); // 等价的调用
-
函数指针形参
函数指针可以作为函数的形参使用,当把函数名作为实参时,将会自动的转换成指针bool lengthCompare(const string &, const string &);//函数原型 void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));//第三个参数是函数类型,会自动转换成函数指针 void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));//等价的定义,显示的将形参定义成指向函数的指针 useBigger(s1,s2,lengthCompare);//自动将函数 lengthCompare 转换成指向该函数的指针
-
使用 typedef
使用类型别名可以简化函数指针的代码。void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &)); typedef bool (*FuncP)(const string &, const string &)// FuncP 是 bool(const string &, const string &) 的类型别名 void useBigger(const string &s1, const string &s2, FuncP);// useBigger的等价声明,使用了类型别名
-
回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。#include <stdio.h> //回调函数 int ADD(int (*callback)(int,int), int a, int b){ return (*callback)(a,b);//此处回调add函数... } //普通函数 int add(int a, int b){ return a + b; } int main(void){ printf("%d\n",add(1,2)); printf("%d\n",ADD(add,1,2)); return 0; }
-
dll 中函数调用
动态调用 dll 库中函数就是通过函数指针实现的。//动态库中方法 extern "C" __declspec(dllexport) int add(int a, int b); int add(int a, int b) { return a + b; } //调用实现 HMODULE module = LoadLibrary("DLLTest1.dll"); if (module == NULL) { return; } typedef int(*AddFunc)(int, int); // 定义函数指针类型别名 AddFunc add; // 定义函数指针变量 add = (AddFunc)GetProcAddress(module, "add");// 获取导出函数地址,并赋值 int sum = add(100, 200);// 通过函数指针调用函数
指针和内存管理
- 基本概念
①.内存分类- 全局/静态内存:用来保存全局变量和静态变量,如局部 static 对象、类 static 数据成员以及定义在任何函数之外的变量;
- 栈内存:用来保存定义在函数内的非 static 对象,如局部变量、函数参数等;
- 堆内存:用来保存动态分配的对象,即通过 new 获得的内存;
- 常量存储区:用来存放常量,不允许更改
②.对象的生命周期 - 全局对象:程序启动时创建,程序结束时销毁;
- 局部对象:进入其定义所在的程序块时创建,离开块时销毁;
- 局部 static 对象:在第一次使用前创建,程序结束时销毁;
- 动态对象:生命周期由程序来控制,必须显示的进行销毁
③.内存泄漏
内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
- 使用 new 和 delete
C++ 定义了两个运算符来分配和释放动态内存。使用 new 分配内存,delete 释放 new 分配的内存。
①.new 运算符
new 运算符会向系统申请足够的存储空间,如果申请成功,返回该内存块的首地址,如果申请失败,会抛出异常,返回零值。
new 运算符返回的是一个指向所分配类型变量(对象)的指针。对所创建的变量或对象,都是通过该指针来间接操作的。new int;//开辟一个存放整数的存储空间,返回一个指向该存储空间的地址,即指针 new int(100);//开辟一个存放整数的空间,并指定该整数的初值为100,返回一个指向该存储空间的地址 new int[10];//开辟一个存放整数数组(包括10个元素)的空间,返回首元素的地址
当执行 new 操作时,一般分为两个过程:一是内存空间被分配出来,二是针对此内存会有一个构造函数被调用。float *p=new float (3.14159); //开辟一个存放单精度数的空间,并指定该实数的初值为//3.14159,将返回的该空间的地址赋给指针变量p
②.delete 运算符
在动态内存使用完毕后,必须将其归还给系统;通过 delete 来将动态内存归还给系统。
删除一个指针 p(delete p)实际意思是删除了 p 所指的目标(变量或对象等),释放了它所占的堆空间,而不是删除p本身(指针p本身并没有撤销,它自己仍然存在,该指针所占内存空间并未释放),释放堆空间后,p成了空指针。int *a = new int; delete a; //释放单个int的空间 int *a = new int[5]; delete []a; //释放int数组空间
当执行 delete 操作时,一般分为两个过程:一是针对此内存会有一个析构函数被调用,二是释放此内存。int *a = new int;// a 指向动态内存 delete a; // a 变为无效 a = nullptr; // a 不再绑定任何对象
- 使用 malloc 和 free
在 C 语言中使用 malloc 和 free 进行动态内存的分配。
①.malloc
C 库函数,分配指定大小的内存空间,并返回一个指向它的指针。
malloc 只是对内存进行分配,没有进行初始化;函数返回类型是 void* ,需要进行强制类型转换。void *malloc(size_t size);// 函数原型
②.freeint *pTest = (int*)malloc(10*sizeof(int)); //开辟10个int型的空间大小
C 库函数,释放用 malloc 分配的内存空间。
malloc 也是在堆内存上分配空间,所以也必须进行显示的释放。void free(void *ptr);//函数原型
int *pTest = (int*)malloc(10*sizeof(int)); //开辟10个int型的空间大小 if(pTest != NULL) { free(pTest);//释放内存 pTest = NULL; }
- allocator 类
allocator 类定义在头文件 memory 中,可以将内存分配和对象构造分离开,allocator 也是一个模版。
allocator 类分配的内存是未构造的,不可用直接对这些内存进行赋值和读取。allocator<string> alloc;//可以分配string类型的allocator auto p = alloc.allocate(10);//分配10个string空间,返回首地址 auto q = p; string s; while (cin >> s && q != p + 10) alloc.construct(q++, s);//用输入的值构造元素,在q 位置调用构造函数 while (q != p) alloc.destroy(--q);//逐一销毁元素,在q位置调用析构函数 alloc.deallocate(p, 10);//释放内存空间
- 用对象管理资源
动态内存的使用很容易出现问题,因为确保正确的时间释放内存是极其困难的。有时会忘记释放内存,这种情况下会产生内存泄漏;有时会在尚有指针引用内存的情况下释放它,这种情况会产生引用非法内存的指针。
对象的析构函数总是会被执行的,因此我们可以把资源放进对象内,便可依赖析构函数自动调用的机制确保资源被释放。即在构造的时候获取资源,在析构的时候释放资源。
①.智能指针
为了更容易的使用动态内存,标准库提供了智能指针来管理动态对象,定义在头文件 memory 中。
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。智能指针也是模版。
②.auto_ptr
auto_ptr 对象禁止进行拷贝、赋值操作。auto_ptr<string> p (new string ("Hello World")); cout << *p;// 解引用 p ,输出指向的对象 string *q = p.get();//返回 p 中保存的指针。
③.shared_ptr
shared_ptr 增加了引用计数,只有当指向一个对象的最后一个 shared_ptr 被销毁时,才会自动销毁对象。shared_ptr<string> p = make_shared<string>("Hello World") cout << *p;// 解引用 p ,输出指向的对象 string *q = p.get();//返回 p 中保存的指针。 shared_ptr<string> q(p);//支持拷贝构造