引用是变量的别名,指针就是变量地址的别名。 与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用又有很多不同点:
- 指针本身是一个对象,允许对指针赋值和拷贝。而且在指针的声明周期内它可以先后指向几个不同的对象。
- 指针无须在定义时赋初始值。(不太建议这个做法)和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
一、基本操作
1. 初始化
建议初始化所有指针。 使用未经初始化的指针是引发运行时错误的一大原因。和其他变量一样,访问未经初始化的指针所引发的后果也是无法预计的。通常这一行为将造成程序崩溃,而且一旦崩溃,要想定位到出错位置将是特别棘手的问题。
在大多数编译器环境下,如果使用了未经初始化的指针,则改指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间恰好有内容,而这些内容又被当做了某个地址,我们就很难分清它到底是合法的还是非法的了。
因此建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。 如果是实在不清楚指针应该指向何处,就把它初始化为nullptr
或者0,这样程序就能检测并知道它没有指向任何具体的对象了。
int i = 42;
int *p1 = 0; //等同于int *p1=nullptr;
int *p2 = &i;
int *p3; //不推荐
AI 代码解读
过去的程序还会用到一个名为NULL的预处理变量(preprocessor variable)来给指针赋值,这个变量在头文件cstdlib
中定义,它的值为0。在新标准下,现在的C++程序最好使用nullptr
,同时尽量避免使用NULL
.
2.赋值
在C++里,指针也是个数据对象,所以也支持相互之间的直接赋值。
int i = 42;
int *p1 = i;
int *p2;
p2 = p3;
p2 = 0;
AI 代码解读
此外,要将数字值作为地址来使用,应通过强制类型转换将数字转换为适当的地址类型。
int *pt;
pt = (int *)0xB8000000; //type match
AI 代码解读
3.算术运算
指针是一个用数值表示的地址。因此,您可以对指针执行算术运算。可以对指针进行四种算术运算:++、--、+、-。
假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,让我们对该指针执行下列的算术运算:
ptr++;
AI 代码解读
在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置(即一个基本类型长度单位),即当前位置往后移 4 个字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。其他三个运算符号的原理相同。
4.指针的比较
指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。
int var[MAX] = {10, 100, 200};
int *ptr;
// 指针中第一个元素的地址
ptr = var;
int i = 0;
while ( ptr <= &var[MAX - 1] ){
cout << "Address of var[" << i << "] = ";
cout << ptr << endl;
cout << "Value of var[" << i << "] = ";
cout << *ptr << endl;
// 指向上一个位置
ptr++;
i++;
}
AI 代码解读
5.指向指针的引用
指针是对象,所以存在对指针的引用。
int i = 42;
int *p = &i;
int *&r = p;
cout << "Dereference r value:" << *r << endl;
*r = 50;
cout << "Dereference p value:" << *p << endl;
AI 代码解读
二、指针和动态内存
C++ 程序中的内存分为两个部分:
- 栈(stack):在函数内部声明的所有变量都将占用栈内存。
- 堆(heap):这是程序中未使用的内存,在程序运行时可用于动态分配内存。
很多时候,无法提前预知需要多少内存来存储某个定义变量中的特定信息,所需内存的大小需要在运行时才能确定。指针的一个重要作用是,在运行阶段分配未命名的内存以存储值。
在 C++ 中,可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即new
运算符。如果您不再需要动态分配的内存空间,可以使用delete
运算符,删除之前由new
运算符分配的内存。
A. new
运算符
使用new
运算符来为任意的数据类型动态分配内存的通用语法。下面的例子就是运行阶段为int值分配未命名的内存,并使用指针来访问这个值。我们需要告诉new
,需要为哪种数据类型分配内存:new
将找到一个长度正确的内存块,并返回该内存的地址。我们接下来的事情就是用指针来存储这个地址值。
int *pt = new int; //allocate space for an int
*pt = 1001; //store a value there
AI 代码解读
要特别指出来,pt的值存储在栈(stack)的内存区域中,而new从堆(heap)或自由存储区(free store)的内存区域分配内存。malloc()
函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用malloc()
函数。new
与malloc()
函数相比,其主要的优点是,new
不只是分配了内存,它还创建了对象。
B. delete
运算符
delete
将使用完后的内存归还到内存池里,防止内存被耗尽。一定要配对使用delete
和new
,否则将发生内存泄露(memory leak),也就是,被分配的内存再也无法使用了。如果内存泄露严重,则程序将由于不断寻找更多内存而终止。
int *ps = new int; //ok
delete ps; //ok
int jugs = 5; //ok
int *pi = &jugs; //ok
delete pi; //not allowed, memory not allocated by new
AI 代码解读
注意,使用delete
的关键在于,将它用于new
分配的内存,而不是用于使用了new
的指针。
重要的事情说三遍!!!
只能用delete
来释放使用new
分配的内存,同时对空指针使用delete
是安全的。
只能用delete
来释放使用new
分配的内存,同时对空指针使用delete
是安全的。
只能用delete
来释放使用new
分配的内存,同时对空指针使用delete
是安全的。
C. new
创建动态数组
通常,对于大型数据(如数组、字符串和结构),应使用new
。例如要编写一个程序,它是否需要数组取决于运行时用户提供的信息。在编译时给数组分配内存被称为静态联编(static binding),如果通过声明来创建数组,则在程序被编译时将为它分配内存空间。不管程序最终是否用到数组,数组都在那里占用了内存。而使用new
,如果在运行时需要数组,则创建它;如果不需要,则不创建,还可以在程序运行时选择数组的长度,这被称为动态联编(dynamic binding)。
int *psome = new int[3]; //get a block of 3 ints
psome[0] = 1;
psome[1] = 2;
psome[2] = 3;
std::cout << "psome[0] is " << psome[0]<< std::endl;
psome = psome + 1; //increment the pointer
std::cout << "Now, psome[0] is " << psome[0]<< std::endl;
psome = psome - 1; //point back to beginning; if not, the program will break down
delete []psome; //free a dynamic array
AI 代码解读
二维数组
int **array;
// 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
array = new int *[m];
for(int i = 0; i < m; i++)
{
array[i] = new int [n] ;
}
//释放
for(int i = 0; i < m; i++)
{
delete [] arrar[i];
}
delete [] array;
AI 代码解读
三、函数和指针
A.定义
每一个函数都占用一段内存单元,它们有一个起始地址,指向函数入口地址的指针称为函数指针。
B.说明
- 要注意区分下面两个语句:
int (*p)(int a, int b); //p是一个指向函数的指针变量,所指函数的返回值类型为整型
int *p(int a, int b); //p是函数名,此函数的返回值类型为整型指针
AI 代码解读
- 指向函数的指针变量不是固定指向哪一个函数的,而只是定义了一个类型的变量,它是专门用来存放函数的入口地址的;在程序中把哪一个函数的地址赋给它,它就指向哪一个函数。
void a(int);
void c(int);
void (*b)(int);
b = a;
b = c;
AI 代码解读
- 定义了一个函数指针并让它指向了一个函数后,对函数的调用可以通过函数名调用,也可以通过函数指针调用(即用指向函数的指针变量调用)。在给函数指针变量赋值时,只需给出函数名,而不必给出参数。
int max(int x, int y); //函数max的原型
int (*p)(int a, int b); //指针p的定义
//将函数max的入口地址赋给指针变量p
//p就是指向函数max的指针变量,也就是p和max都指向函数的开头。
p = max;
p(2,4); //效果等同于max(2,4);
AI 代码解读
- 在一个程序中,指针变量
p
可以先后指向不同的函数,但一个函数不能赋给一个不一致的函数指针(即不能让一个函数指针指向与其类型不一致的函数)
//声明函数
int fn1(int x, int y);
int fn2(int x);
//定义函数指针
int (*p1)(int a, int b); int (*p2)(int a);
p1 = fn1; //ok
p2 = fn2; //ok
p1 = fn2; //compile error
AI 代码解读
函数指针只能指向函数的入口处,而不可能指向函数中间的某一条指令。不能用*(p+1)来表示函数的下一条指令。
函数指针变量常用的用途之一是把指针作为参数传递到其他函数。
C.举个栗子
#include <iostream>
using namespace std;
#include <conio.h>
int max(int x, int y); //求最大数
int min(int x, int y); //求最小数
int add(int x, int y); //求和
void process(int i, int j, int (*p)(int a, int b)); //应用函数指针
int main()
{
int x, y;
cin>>x>>y;
cout<<"Max is: ";
process(x, y, max);
cout<<"Min is: ";
process(x, y, min);
cout<<"Add is: ";
process(x, y, add);
getch();
return 0;
}
int max(int x, int y){
return x > y ? x : y;
}
int min(int x, int y){
return x > y ? y : x;
}
int add(int x, int y){
return x + y;
}
void process(int i, int j, int (*p)(int a, int b)){
cout<<p(i, j)<<endl;
}
AI 代码解读
四、void*指针
A.定义
用void*
定义一个void类型的指针,它不指向任何类型的数据,意思是,void*
指针“指向空类型”或“不指向确定的类型”,而不要理解为void*指针能指向“任何的类型”数据。简而言之:void*
只提供一个地址,没有指向。
double obj = 3.14, *pd = &obj;
void *pv = &obj;
pv = pd;
AI 代码解读
B.作用
void*
指针不指向任何数据类型,它属于一种未确定类型的过渡型数据,因此如果要访问实际存在的数据,必须将void*
指针强转成为指定一个确定的数据类型的数据,如int*
、string*
等。不允许使用void*
指针操作它所指向的对象,例如,不允许对void*
指针进行解引用。不允许对void*
指针进行算术操作。
当进行纯粹的内存操作时,或者传递一个指向未定类型的指针时,可以使用void指针。void指针也常常用作函数指针。
在较早版本的C中,通过字符指针(char *)实现的,但是这容易产生混淆,因为人们不容易判断一个字符指针究竟是指向一个字符串,还是指向一个字符数组,或者仅仅是指向内存中的某个地址。在C中,它们被广泛使用,但是在 C++ 我认为它们很少,如果有必要,因为我们有多态性,模板 等等 它提供了一个更清洁和更安全的方法来解决C中的同一个问题。
void*
主要用途:
a.void指针一般用于应用的底层,比如malloc函数的返回类型是void指针,需要再强制转换;
b.文件句柄HANDLE也是void指针类型,这也是句柄和指针的区别;
c.内存操作函数的原型也需要void指针限定传入参数:
void * memcpy (void *dest, const void *src, size_t len);
void * memset (void *buffer, int c, size_t num );
AI 代码解读
d. 面向对象函数中底层对基类的抽象。
// vp1 需要交换的一个数的地址
// vp2 需要交换的另一个数的地址
// size 为两个交换的数的类型大小,通过sizeof来计算
// 这里假设传进来的是相同数据类型的地址,比如两个数都是整数,或者都是字符串等等
void swap(void *vp1, void *vp2, int size)
{
char *buffer = (char *)malloc(size);
memcpy(buffer, vp1, size);
memcpy(vp1, vp2, size);
memcpy(vp2, buffer, size);
free(buffer);
}
int main()
{
double a = 1.2;
double b = 0.9;
swap(&a, &b, sizeof(double));
return 0;
}
AI 代码解读
五、const和指针
A.指向常量的指针
指向常量的指针,它所指向的内容(即地址)可变,但这个地址里的内容不可变。
const double pi = 3.14; //pi is const double variable
double *ptr = π //error:ptr is normal pointer
const double *cptr = π //ok: cptr is a double const pointer
*cptr = 4.2; //error:cannot assign value to const variable
const double pp = 6.28;
cptr = &pp;
AI 代码解读
B.const指针(常量指针)
指针是对象而引用不是,因此就像对其他对象类型一样,允许把指针本身定为常量。常量指针必须初始化,而且一旦初始化完成,则它的值(存放在指针中的那个地址)就不能在改变了。
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全取决于所指对象的类型。
int errNum = 0;
int *const curErr = &errNum;
const double pi = 3.14;
const double *const pip = π
*pip = 2.72; //error
cout << curErr << endl; //0
(*curErr)++; //okay
cout << errNum << endl; //1
AI 代码解读
参考文献
- 《C++ Primer(第5版)》 Stanley B.Lippman, Josee Lajoie, Barbara E.Moo
- 《C++ Primer Plus(5th Edition)》
- void和void *
- C++函数指针详解