一、指针的概念
1. 计算机体系中的存储层次
在介绍指针的概念之前,先介绍一下计算机中的存储层次。
L5 —— Remote secondary storage:
网络磁盘,在存储体系中相对比较慢。是在不同的机器,甚至不同的网段之间传输数据。从存储角度来看,假设把数据存储到多台机器上,或者是通过互联网去访问某些信息的时候,我们可以认为整个世界就是一张大的磁盘。
L4 —— Local secondary storage:
本地磁盘,访问速度比网络磁盘快些,但容量没有网络磁盘大。
L3 —— Main memory:
主存(实际上就是内存),相对磁盘而言,容量更小,但传输数据的速度更快。主存中一旦断电,数据就会丢失。
L2、L1 —— chahe:
高速缓冲存储器,速度相对主存来说更快了。
L0 —— Registers:
寄存器,比L1更快。可以直接接触CPU中的处理数据,寄存器中的数据也是可以直接拿来运算的。
整个金字塔体系结构中,越往下速度越慢,容量越大,越往上速度越快,容量越小。
2. C++中内存单元内容与地址
- 内存由很多内存单元组成。这些内存单元用于存放各种类型的数据。
- 计算机对内存的每个内存单元都进行了编号,这个编号就是内存地址,地址决定了内存单元在内存中的位置。
- 记住这些内存单元地址不方便,C++语言的编译器可以让我们通过名字来访问这些内存位置。
举例来说明:
int a = 112, b = -1;
float c = 3.14;
// 指针变量
int* d = &a;
float* e = &c;
代码中的变量在内存中的存储:
首先,假设内存当中有五个连续的内存块,每个内存块容量为32位,也就是4个bit。内存地址分别为116、112、108、104、100,分别映射了a、b、c、d、e各个变量。C++编译器可以通过变量名直接定位到各个变量的内存地址。
变量d和e中存储的是变量a和c的内存地址,也就是a和c在内存中的位置。当我们取d和e的值时,可以发现这两个变量的值不是一般意义上的类似整形或浮点型的数值,实际上存储的是内存地址,这样的变量就叫做指针。
可以看作是一种间接访问的方法,在C++程序中有着非常重要的作用。
下面就来正式认识一下指针。
3. 指针的定义和间接访问操作
3.1 指针定义
- 指针定义的基本形式:
指针本身就是一个变量,其符合变量定义的基本形式。和普通变量不同的是,它存储的是值的地址。对类型T,T*是“到T的指针”类型,一个类型为T*的变量,能保存一个类型T的对象的地址。
3.2 间接访问操作
- 通过一个指针,访问它所指向地址的过程称为间接访问(indirection)或者引用指针(dereferencing the point):
这个用于执行间接访问的操作符为单目操作符:*
比如:
#include <iostream>
using namespace std;
int main()
{
int a = 112, b = -1;
float c = 3.14;
// 指针变量
int* d = &a;
float* e = &c;
cout << (*d) << endl;
cout << (*e) << endl;
return 0;
}
假设在内存中的存储情况为:
输出结果为:
112
3.14
为了进一步理解,可以运行以下代码,根据结果来加深指针的定义:
#include <iostream>
using namespace std;
int main()
{
int a = 112, b = -1;
float c = 3.14;
// 指针变量
int* d = &a;
float* e = &c;
cout << "当前d,e的内存地址:" << endl;
cout << " " << (&d) << endl;
cout << " " << (&e) << endl;
cout << "取d,e的值:" << endl;
cout << " " << (d) << endl;
cout << " " << (e) << endl;
cout << "当前a,c的内存地址:" << endl;
cout << " " << (&a) << endl;
cout << " " << (&c) << endl;
cout << "取a,c的值:" << endl;
cout << " " << (a) << endl;
cout << " " << (c) << endl;
return 0;
}
输出结果为:
当前d,e的内存地址:
0x7fffe0180538
0x7fffe0180540
取d,e的值:
0x7fffe018052c
0x7fffe0180530
当前a,c的内存地址:
0x7fffe018052c
0x7fffe0180530
取a,c的值:
112
3.14
可以明显看出,指针变量d,e中存储的是变量a,c的地址。通过指针间接访问a,c的值,也就是取*d和*e的值。要区别开加*和不加*。比如d为变量a的地址,而*d则通过变量a的地址取到了a的值。一个是地址,一个是值。
(要注意一个问题,输出变量的内存地址时,每运行一次程序,输出的地址都很有可能不同,所以尽量让他们在一个程序中输出结果便于对比。)
4. 小结
- 一个变量有三个重要信息:
变量的地址位置
变量所存的信息
变量的类型
- 指针变量是一个专门用来记录变量地址的变量,可以间接访问另一个变量的值。
二、C++的原生指针
1. 数组与指针
字符串本身就是一个字符型的数组,它还有另外一种形式的定义,或者说是另外一种类型的展现方式,即指针的形式:
- strHello不可变,strHello[index]的值可变
- pStrHello可变,pStrHello[index]的可变不可变取决与所指区间的存储区域是否可变
或者说可以这样理解:数组本身指向的存储空间是不允许改变的,而数组的值是允许改变的;指针指向的存储空间是允许改变的,而其指向的值可变不可变取决与所指区间的存储区域是否可变。
在编译器中运行时,会有提示上面的代码有错误,错误提示为:表达式必须是可修改的左值。
何为左值呢?
2. 左值与右值
2.1 概念
一般说法,左值(lvalue:locator value)是编译器为其单独分配了一块存储空间,可以取其地址,左值可以放在赋值运算符左边,也可放在右侧;右值(rvalue)指的是数据本身,数据的一种表述方式,编译器不会为其单独分配地址空间,也就不能取到其自身地址,右值只能赋值运算右边。
也就是说,两者区别:是否是在内存中占有确定位置的对象。
左值最常见的情况比如说作为函数和数据成员的名字;
右值是没有标识符、不可以取代地址的表达式,一般也称之为“临时对象”或“临时结果”。
2.2 举例说明
举一个最基本的例子,定义一个整形变量并给它赋值:
int a;
a = 3;
赋值运算符要求一个左值作为它的左操作数,在这里a是一个占确定内存空间的对象,也就是说a在这个代码中是作为一个左值的,代码有效。
再看下面的代码:
1 = b;
a + 1 = 4;
很明显常量1和表达式a + 1是作为表达式的临时结果,只是计算的周期驻留在临时的寄存器中,没有确定的内存空间,无法给其赋值,此代码无效。
再举一个稍微复杂的例子,比如下面代码:
int foo()
{
return 2;
}
int main()
{
foo() = 2;
return 0;
}
这个代码是不合法的,foo返回的是一个临时的右值,尝试给其赋值是一个错误。
但也不是所有的对调用函数赋值都是无效的。C++中的引用(reference)让对调用函数赋值成为可能:
int g = 20;
int& foo()
{
return g;
}
int main()
{
foo() = 10;
return 0;
}
(return g 和 & 少一个都会出现错误)
这里foo返回一个引用,并非一个临时的值,其作为左值可以被赋值。
实际上,C++从函数中返回左值的能力对于实现一些重载运算符时很重要。比如,在类中为实现某种查找访问而重载中括号运算符[ ]。std::map可以这样做:
std::map<int, float> mymap;
mymap[10]=5.6;
给mymap[10]赋值合法是因为非const的重载运算符std::map::operator[ ]返回一个可以被赋值的引用。
注意,也不是所有的左值都能被赋值。比如:
const int a = 10; //‘a’是一个左值
a = 10; //但是它不能被赋值
C标准中添加了const关键字后,正式的,C99标准定义可修改左值为:
[…] 一个左值没有数组类型,没有不完全类型,没有const修饰的类型,并且如果它是结构体或联合体,则没有任何const修饰的成员(包含,递归包含,任何成员元素的集合)。
想要更深入了解左值与右值,可以参考此博客:理解C和C++中的左值和右值
三、几种C++中的原始指针
1. 一般类型指针T*
T是一个泛型,泛指任何一种类型。
如int型、double型、char型等等:
int i = 4;
int* ip = &i;
cout << (*ip) << endl;
double d = 3.14;
double* dp = &d;
cout << (*dp) << endl;
char c = 'a';
char* cp = &c;
cout << (*cp) << endl;
输出:
4
3.14
a
不必纠结T的含义,它在这里就是一个代称,代指一般见到的指针类型,比如上面列举的int、double、char,以及其他的未列举出来的类型。
2. 指针数组与数组指针
2.1 定义
- 指针数组(array of pointers)
所谓指针数组,顾名思义,首先这是一个数组,数组的类型为指针类型。也就是说指针数组是一个存放指针变量的数组,里面的每一个元素都是指针变量。
- 数组指针(a pointer to an array)
从这个名字来看,很明显这是一个指针,而它的指针类型,也就说这个指针的指向,是一个数组。现在就可以看出与一般指针类型的区别了,一般指针指向的是单个的变量,比如int,double这种,一个变量里只能存一个值,而数组指针则指向了一个数组。
2.2 表达式
- 指针数组:T* t[ ]
注意,[ ]的优先级较高,所以t [ ]就表示一个名为t的数组,前面的T*,为了容易理解,我们可以换成int*,类比单个指针变量的定义,比如:int* t,表示一个名为t的int型指针变量,那int* t[ ],就表示这是一个名为int型的指针数组,数组里的每一个元素都是一个指针变量。
- 数组指针:T(*t) [ ]
加上括号,先表示这是一个名为t的指针变量,类型是T,后面的[ ]则表示这个指针变量指向的是一个数组。
2.4 举例
int A = 1,B = 2,C = 3;
int* a[3] = {&A, &B, &C};
for(int i = 0; i < 3; i++)
cout << *(a[i]) << endl;
int d[3] = {A, B, C};
int(*b)[3];
b = &d;
for(int i = 0; i < 3; i++)
cout << (*b)[i] << endl;
输出:
1
2
3
1
2
3
3. const与指针
先看一下下面的代码:
char Str[] ={"Hello"};
char const *pStr1 = "Hello";
char* const pStr2 = "Hello";
char const *const pStr3 = "Hello";
第一眼看到的时候会有点懵,下面先来介绍一下const。
const本身是一种规范,当有一个变量被const所修饰时,就告诉编译器这个变量在程序运行期间是不可被改变的,是一个不变的值。当普通的定义时,我们可以在程序的开始定义某个变量的值为3,比如int i = 3,但在程序运行中,i的值是允许被改变的,比如i++之后,i的值就变成了4。而当用const修饰变量时,这个变量即便是在程序运行中,也是不允许改变的。
当const与指针放在一起时,我们该如何分辨它们所表示的含义呢?
当const与指针放在一起时,表示的仍是个指针,只是和普通的指针变量相比,指针值可变不可变的问题。所以主要还是要弄明白const修饰的是什么,也就代表着什么值不可改变。
关于const修饰的部分,有一个小规则:1. 看左侧最近的部分;2. 如果左侧没有,则看右侧。
比如第一个:
char const *pStr1 = "Hello";
const左侧最近的是char,这就代表const修饰的就是char。
这句代码定义了一个指针,它指向的是“Hello”的地址,由于const修饰的是char,也就说这个char变量的内容不可改变,也就是说,“Hello”的值不可改变,可改变的只是存储“Hello”的地址。
而第二个:
char* const pStr2 = "Hello";
可以看到,const左侧最近的是*,也就是说const修饰的一个指针。
pStr2指向的是“Hello”的地址,由于const修饰的是指针,所以指针的内容不能变,也就是说指针指向的地址是固定的,不发生变化,可改变的是地址所对应的内容。
最后一个:
char const *const pStr3 = "Hello";
首先,第一个const左侧最近是char,与第一个例子相同,这代表的是指针指向的char变量内容不能改变;其次,第二个const左侧最近的是*,与第二个例子相同,表示指针指向的地址不能发生改变;最后,将两个const放在一起就表示这个指针变量所存储的地址不能发生改变,地址对应的值也不能发生改变。
直观来看,如果将下面程序中几行带注释的代码的注释去掉,程序就会报错。
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
char Str[] ={"Hello"};
char const *pStr1 = "Hello";
char* const pStr2 = "Hello";
char const *const pStr3 = "Hello";
pStr1 = Str;
//pStr2 = Str;
//pStr3 = Str;
unsigned int len = strlen(pStr2);
cout << len << endl;
for(unsigned int i = 0; i < len; i++)
{
//pStr1[i] += 1;
pStr2[i] += 1;
//pStr3[i] += 1;
}
return 0;
}
同时,也有可能不这样定义。平时见得比较多的还是const放在前面,比如第一和第三个:
const char *pStr1 = "Hello";
const char *const pStr3 = "Hello";
在前面部分,const和char是可以颠倒的。这样两种定义方法表达的含义相同,使用哪一个都可以。
4. 指向指针的指针
举例:
int a = 123;
int *b = &a;
int **c = &b;
5. 关于野指针
5.1 未初始化和非法的指针
比如:
int *a;
*a = 12;
用指针进行间接访问之前,一定要非常小心,确保它已经初始化,并被恰当的赋值。
5.2 NULL指针
如:
int *a = NULL;
一种特殊指针,表示不指向任何东西。
注意:
- 对于一个指针,如果已经知道将被初始化为什么地址,那么请赋给它这个地址值,否则请把它设置为NULL;
- 在对一个指针进行间接引用前,请先判断这个指针的值是否为NULL
例如:
#include "stdafx.h"
#include <iostream>
using namespace std;
int main()
{
// 指针的指针
int a = 123;
int* b = &a;
int** c = &b;
// NULL的使用
int* pA = NULL;
pA = &a;
if (pA != NULL) // 判断NULL指针
{
cout << (*pA) << endl;
}
pA = NULL; // pA不用时,置为NULL
return 0;
}
5.3 杜绝野指针
指向“垃圾”内存的指针。
什么是垃圾指针:
指针变量没有初始化;
已经释放不用的指针没有置NULL,如delete和free之后的指针;
指针操作超越了变量的作用范围;
指针使用的注意事项:
- 没有初始化的,不用的或者超出范围的指针请把值置为NULL.
6. 指针的基本运算
6.1 &和*操作符
取地址和间接访问的操作符。
char ch = 'a';
char* cP = &ch;
有一个char型变量ch,一个char型指针变量cP,(在当前所有的系统中,所有的指针默认情况下都是四个字节,16进制的整形数,不管是int型指针还是char型指针,占用的字节数都是一样的,表现形式也是一样的。)将ch的地址赋给指针cP。