8 指针
1 指针变量的声明和初始化
间接引用
指针变量把内存地址作为它们的值。
通常,一个变量直接包含一个特定的值。但是一个指针包含的是一个变量的内存地址,而该变量包含了一个特定的值。因此从这个意义上来讲,一个变量名直接引用一个值,而一个指针间接引用一个值。
通过指针引用值称为间接引用。
指针的声明
和其他任何变量一样,指针在使用前必须声明。
int *countPtr;
该语句声明了变量countPtr
是int *
类型的(即一个指向int
值的指针),读作“countPtr
是一个指向int
的指针”。
每一个声明为指针的变量在变量名前面必须有一个星号(*
)。例如:
double *xPtr,*yPtr;
指针可以被声明为指向任何数据类型的对象。
TIPS:
虽然不是必要的,但指针变量名中包含字母“Ptr”就可以清楚地表明这些变量是指针。
指针的初始化
指针在声明或赋值时,应该被初始化为nullptr
(这是C++11的新特性)或一个相应类型的地址。
一个值为nullptr
的指针“指向空”,被称为空指针。
对所有的指针应该进行初始化,以防止指向一个未知的或未被初始化的内存空间。
2 指针运算符
地址运算符 &
地址运算符&
是一个一元运算符,它获得操作数的内存地址。例如:
int y = 5;
int *yPtr = nullptr;
yPtr = &y;
这里把变量y的地址赋值给指针变量yPtr。
地址运算符的操作数必须是左值,不能用于常量(无论是数值还是常量的标识符)或者结果是临时值(如计算的结果)的表达式。
间接运算符 *
一元的*
运算符通常称为间接运算符或间接引用(dereference)运算符,它返回的是一个左值,表示其指针操作数所指向的对象。如:
cout << *yPtr << endl;
cout << y <<endl;
这两行语句是等价的。
以第一行的方式使用*
称为间接引用一个指针。
间接引用的指针可以在赋值运算符左侧使用,如:
*yPtr = 9;
间接引用的指针可以来接收输入的值,如:
cin >> *yPtr;
它把输入的值存放到y中。
在间接引用一个指针前要确定它是非空的。
使用地址运算符和间接引用运算符
#include <iostream>
using namespace std;
int main()
{
int a = 7;
int *aPtr = &a;
cout << "The address of a is " << &a << "\nThe value of aPtr is " << aPtr;
cout << "\n\nThe value of a is " << a << "\nThe value of *aPtr is " << *aPtr;
}
该程序输出的十六进制内存地址取决于系统平台。
执行结果如下:
The address of a is 00000081212FF7A4
The value of aPtr is 00000081212FF7A4
The value of a is 7
The value of *aPtr is 7
地址运算符和简介引用运算符优先级与!
同级:
运算符 | 结合律 | 类型 |
---|---|---|
:: () | 从左向右 | 最高 |
++ -- static_cast<类型>() | 从左向右 | 后缀 |
++ -- + - ! & * | 从右向左 | 一元(前缀) |
* / % | 从左向右 | 乘 |
+ - | 从左向右 | 加 |
<< >> | 从左向右 | 插入/提取 |
< <= > >= | 从左向右 | 关系 |
== != | 从左向右 | 相等 |
&& | 从左向右 | 逻辑与 |
|| | 从左向右 | 逻辑或 |
?: | 从右向左 | 条件 |
= += -= *= /= %= | 从右向左 | 赋值 |
, | 从左向右 | 逗号 |
3 使用指针的按引用传递方法
C++中有三种向函数传递参数的方法 —— 按值传递、使用引用参数的按引用传递和使用指针参数的按引用传递。
和引用一样,指针也可用于修改调用者中的一个或多个变量,或者可以将指向大型数据对象的指针传递给函数,从而避免按值传递对象所需要的开销。
按值传递的一个例子
#include <iostream>
using namespace std;
int cubeByValue(int);
int main()
{
int number = 5;
cout << "The original value of number is " << number;
number = cubeByValue(number);
cout << "\nThe new value of number is " << number << endl;
}
int cubeByValue(int n)
{
return n * n * n;
}
使用指针参数的按引用传递的一个例子
#include <iostream>
using namespace std;
void cubeByReference(int *);
int main()
{
int number = 5;
cout << "The original value of number is " << number;
cubeByReference(&number);
cout << "\nThe new value of number is " << number << endl;
}
int cubeByReference(int *nPtr)
{
*nPtr = *nPtr * *nPtr * *nPtr;
}
4 内置数组
内置数组也是固定大小的数据结构。
声明内置数组
语法格式为:
类型 数组名[数组大小];
例如:
int c[12];
这样编译器将保留大小合适的内存空间。
数组大小必须是个大于0的整数常量。
访问内置数组的元素
和array对象一样,使用下标运算符[]
来访问内置数组的单个元素,内置数组中下标运算符[]
也不提供边界检查功能。
初始化内置数组
通过初始化列表可以初始化内置数组的元素。如:
int n[5] = {50,20,30,10,40};
如果提供的初始化值少于元素个数,对剩下的元素而言,基本的数值类型的元素设置为0,bool
类型的设置为false
,指针设置为nullptr
,类的对象被它们的默认构造函数来初始化。
如果提供的初始化值多了,则产生编译错误。
如果一个内置数组的声明有初始化列表但数组的大小是省略的,那么编译器将这个内置数组的大小设置为初始化列表中的元素个数。如:
int n[] = {50,20,30,10,40};
TIPS:
应该总是指定内置数组的大小,甚至在提供了初始化列表的时候。这使编译器能够确保并没有提供过多的初始化值。
将内置数组传递给函数
内置数组的名字的值可隐式地转换为这个内置数组第一个元素的内存地址。因此若内置数组名是arrayName
,则它可隐式地转换为&arrayName[0]
。出于这个原因,则不需要(用&
)取内置数组的地址来把它传递到函数,只需要简单传递内置数组名即可。
一个函数如果接收的是调用函数中一个变量的指针,那么该函数就可以修改调用函数中的这个变量。对于内置数组而言,这意味着被调函数可以修改调用函数中一个内置数组的所有元素,除非被调函数在相应的内置数组形参前加const
限定,以表明这些元素不应该被修改(事实上最好这样)。
声明内置数组形参
在函数头部可以声明内置数组形参,形式如下:
int sumElements(const int values[], const size_t numberOfElement)
这表明这个函数的第一个参数应该是一个一维的具有int
元素的内置数组,并且该数组不应该被这个函数修改。不同于array
对象,内置数组不知道它们自己的大小,因此处理内置数组的函数应当具有接收内置数组及其大小的相应形参。
上述的函数头部还可以写成:
int sumElements(const int *values, const size_t numberOfElement)
编译器并不能区分接收一个指针的函数和接收一个内置数组的函数。
当编译器遇到形如const int values[]
的一维内置数组的函数形参时,它将这个形参转换成指针的表示形式 const int *values
(也就是说,“values
是指向一个整数常量的指针”)。声明一个一维内置数组形参的这两和形式是可互换的。不过为清楚起见,在函数期望的实参是一个内置数组时,应该使用[]
的表示形式。
C++11:标准库函数begin和end
下面语句对名为colors
的一个有string
类型元素的array
对象进行排序。
sort(colors.begin(), colors.end());
函数sort也可以应用于内置数组。如:
sort(begin(n),end(n));
内置数组的局限性
- 它们无法使用关系和相等运算符进行比较,不能相互赋值;
- 它们不知道自己的大小。处理一个内置数组的函数通常接收的实参包括这个内置数组的名字和它的大小;
- 它们不提供自动边界检查的功能。
与内置数组相比,类模板array
和vector
的对象更安全,并且提供了更多的功能。
5 使用const修饰指针
将指针传递给函数有4种方式:
-
指向非
const
数据的非const
指针并不包含
const
修饰符,具有最大访问权限。 -
指向
const
数据的非const
指针可以被修改以指向任何适当类型的其他数据项,但不能通过该指针来修改它所指向的数据。
void f(const int*); int main() { int y = 0; f(&y); } void f(const int *xPtr) { *xPtr = 100; //Error: cannot modify a const object }
-
指向非
const
数据的const
指针可以通过该指针修改这个位置上的数据。声明为
const
的指针必须在它们被声明时进行初始化。int main() { int x, y; int* const ptr = &x; *ptr = 7; ptr = &y; //Error: ptr is a const; cannot assign to it a new address }
-
指向
const
数据的const
指针具有最小的访问权限。
int main() { int x = 5, y; const int* const ptr = &x; cout << *ptr << endl; *ptr = 7; //Error: *ptr is a const; cannot assign to it a new value ptr = &y; //Error: ptr is a const; cannot assign to it a new address }
6 sizeof运算符
C++的一元运算符sizeof
,在程序编译期间确定内置数组,或者任何其他数据类型、变量或常量的字节大小。
当sizeof
运算符应用到一个内置数组名时,返回这个内置数组的总字节数,返回值是size_t
类型。
当sizeof
运算符作用到以内置数组作为实参的函数的指针形参时,返回这个指针的字节数(在此使用的系统中这个值是8),而不是该数组的大小。
#include <iostream>
using namespace std;
size_t getSize(double*);
int main()
{
double numbers[20];
cout << "The number of bytes in the array is " << sizeof(numbers);
cout << "\nThe number of bytes returned by getSize is " << getSize(numbers) << endl;
}
size_t getSize(double* ptr)
{
return sizeof(ptr);
}
The number of bytes in the array is 160
The number of bytes returned by getSize is 8
使用两个sizeof
运算的结果就可以确定一个内置数组的元素个数。如:
sizeof numbers / sizeof(numbers[0]);
sizeof
运算符可应用于任何表达式或者任何类型名。
当sizeof
应用于一个变量名(不是一个内置数组名)或其他表达式时,返回的是用于存储该表达式的特定类型的字节数。
类型名(例如int
)作为sizeof
的操作数时,必须要使用圆括号。当sizeof
的操作数是常量时,它可不用圆括号。当sizeof
的操作数是表达式时,需要用圆括号,但因为sizeof
是一个编译时运算符,它的操作数不会被求值。
#include <iostream>
using namespace std;
int main()
{
int a = 10;
cout << sizeof(int) << endl;
cout << sizeof a << endl;
cout << sizeof(++a) << endl;
cout << a << endl;
}
4
4
4
10
7 指针表达式和指针算术运算
一个指针可以自增( ++
)或自减( --
),可以加上( +
或+=
)一个整数,可以减去( -
或-=
)一个整数,或者一个指针可以减去另一个同类型的指针(这一特殊的运算只适用于指向同一内置数组元素的两个指针)
假设已声明了内置数组int v[5]
,并且它的第一个元素在内存位置3000处。又假设指针vPtr
已初始化指向v[0]
(即vPtr
的值是3000)。
对一个用4字节存储整数的机器,可以用下面任何一条语句把vPtr
初始化为指向数组v
(因为内置数组的名字的值就是其第0元素的地址):
int *vPtr = v;
int *vPtr = &v[0];
指针加上和减去整数
当一个指针加上或减去一个整数时,它不是简单地加上或减去这个整数,而是加上或减去这个整数与该指针指向对象的字节大小的乘积。字节数取决于对象的数据类型。如:
vPtr += 2;
会得到3008(由计算3000+2*4而来),假设存储一个int
数据需要4字节内存。此时,在内置数组中v
中,vPtr
指向v[2]
。
如果指针加1或减1,则可以使用自增或自减运算符。如:
++vPtr;
vPtr++; //每一条都是将指针加1,使它指向内置数组下一个元素
--vPtr;
vPtr--; //每一条都是将指针减1,使它指向内置数组前一个元素
TIPS:
指针的算术运算是没有边界检查功能的。必须确保每一个指针算术运算,即加上一个整数或减一个整数,产生的结果指针所指向的元素必须在内置数组的边界内。
指针相减
指向同一个内置数组的指针变量可以相减。例如,如果vPtr
包含地址3000,v2Ptr
包含地址3008,那么下述语句:
x = v2Ptr - vPtr;
将把从vPtr
到v2Ptr
的内置数组元素的个数赋值给x
,在这里为2。指针算术运算只有在指向内置数组的指针上进行时才有意义。我们无法假设相同类型的两个变量会连续地存储在内存中,除非它们是一个数组的相邻元素。
将两个不指向同一内置数组元素的指针相减或进行比较,是一个逻辑错误。
指针赋值
如果两个指针是同一类型的,那么可以把一个指针赋值给另一个指针。否则,必须用强制类型转换运算符(通常是用reinterpret_cast
),将赋值运算符右侧的指针值转换为赋值运算符左侧的指针类型,否则会造成编译错误。
这个规则有一个例外,就是void
指针(即 void*
,它是一种通用指针,可以表示任何指针类型。任何指向基本类型或类类型的指针都可以被赋值给void*
类型的指针,而不需要进行强制的类型转换。但是,void*
类型的指针是不可以直接赋值给其他类型的指针的,必须先把void*
类型的指针强制转换为适合的指针类型。
不能间接引用 void* 指针
void*
指针不能被间接引用。例如,编译器“知道”在4字节整数的机器中一个int
指针指向的是4字节内存,但是,void
指针只是包含一个未知数据类型的内存地址,编译器不知道该指针所指向的确切字节数和数据类型。编译器必须知道特定指针的数据类型,才能确定该指针间接引用的字节数。对于void
指针,无法确定这样的字节数。
指针比较
指针可以使用相等和关系运算符进行比较。
只有在指针指向同一数组的元素时,使用关系运算符对它们进行比较才是有意义的。指针比较是比较存储在指针中的地址。
例如,比较指向同一数组的两个指针可以发现,指向内置数组中下标编号大一些的元素的指针比另一个指向下标编号小一些的元素的指针大。
一个常用的指针比较是判定一个指针的值是否为nullptr
、0
或者NULL
(即没有任何所指的指针)。
8 指针和内置数组之间的关系
假设有以下的声明:
int b[5];
int *bPtr;
我们可以用下面任一语句将bPtr
设置为内置数组b
中第一个元素的地址:
bPtr = b;
或
bPtr = &b[0];
指针/偏移量表示法
引用内置数组元素b[3]
的另一种方法是采用下面的指针表达法:
*(bPtr + 3);
该表达式中的3是距离指针bPtr
的偏移量(offset)。当该指针指向内置数组的首元素时,偏移量表示应该引用内置数组的某个元素,并且偏移量的值和该内置数组元素的的下标是相同的。这种表示法称为指针/偏移量表示法。
正像内置数组元素可以用指针表达式引用一样,下面的地址:
&b[3];
可以写成如下的指针表达式形式:
bPtr + 3;
以内置数组名作为指针的指针/偏移量表示法
内置数组名可以当作指针并可以在指针算术运算中使用。如:
*(b + 3);
也引用内置数组元素b[3]
。前面的表达式并没有以任何方式修改内置数组名,b
仍然指向内置数组中的第一个元素。
指针/下标表示法
和内置数组一样,指针也可以带下标。如:
bPtr[1];
引用内置数组元素b[1]
。
内置数组名不可修改
b += 3;
这个表达式会引起编译错误。
9 基于指针的字符串
字符常量
一个字符常量就是一个整数值,表示为用一对单引号引起来的字符。字符常量的值是机器字符集中该字符的整数值。
字符串
一个字符串是一个被视为整体的字符序列。字符串可以包括字母、数字和各种特殊字符(如+
-
*
/
$
等)。
在C++中,字符串文字(string literal)或字符串常量都写在一对双引号中。
基于指针的字符串
基于指针的字符串是一个以空字符('\0'
)结尾的内置字符数组,这个空字符标记了字符串在内存中结束的位置。
通过指向字符串第一个字符的指针来访问该字符串。对一个字符串文字进行sizeof
运算得到的是包含结束的空字符在内的这个字符串的长度。基于指针的字符串和内置数组一样,内置数组名也是指向该内置数组第一个元素的指针。
字符串文字作为初始化值
无论是在内置字符数组的声明中,还是在const char*
类型的变量的声明中,都可以将字符串文字作为初始化值。如:
char color[] = "blue";
const char *colorPtr = "blue";
第一个声明创建了一个具有5个元素的内置数组color
,它包含字符'b'
'l'
'u'
'e'
和 '\0'
。第二个声明创建了指针变量colorPtr
,它指向在内存某处的字符串"blue"
(以'\0'
结尾)中的字母b
。字符串文字是static
存储类别的(它们在程序执行时间内一直存在),如果程序中有多个地方引用同一个字符串文字,那么它可以被共享,也可以不被共享。
字符常量作为初始化值
char color[] = {'b','l','u','e','\0'};
其中使用单引号引起来的字符常量作为内置数组的每个元素的初始化值。
当声明一个内置的字符数组来包含一个字符串时,这个内置数组应该足够大,从而保证可以存诸该字符串和它的结束空字符。编译器会根据初始化列表中初始化值的个数,来决定上述声明中内置数级组的大小。
访问C字符串的字符
一个C字符串是一个内置的字符数组,所以可以用内置数组的下标表示法直接访问字符串中单个的字符。如color[0]
是字符'b'
。
使用cin读取字符串到char类型的内置数组中
可以用下面语句读取一个字符串到名为word
的内置字符数组中,该数组有20个元素:
cin >> word;
用户输入的字符串存储在word
中。上述的语句读入字符,直到遇到空白字符或文件结束符为止。
请注意,这个字符串的长度不能超过19个字符,以便为结束空字符留出空间。
还可以用setw
流操纵符来保证读入word
的字符串不会超过内置字符数组的长度。如:
cin >> setw(20) >> word;
指定cin
最多应读取19个字符到内置数组word
中,保留内置数组组中的第20个位置用于存储字符串的结束空字符。
setw
流操纵符不是黏性设置,因此只作用于下一个要输人的值。如果输入了多于19个的字符,剩下的字符不会被存储在word
中,但会在输入流中能够被下一次的输入操作所读入。
使用 cin.getline 读取文本行到 char 类型的内置数组中
有时,需要输入一整行文本到一个内置数组中。为此,C++的 cin
对象提供了函数getline
。该函数有三个参数:一个存储该行文本的内置字符数组、一个长度和一个定界字符。如:
char sentence[80];
cin.getline(sentence,80,'\n');
声明了一个具有80个字符的内置数组sentence
,并从键盘读人一行行文本到这个内置数组中。当遇到定界字符'\n'
,或者当输入了文件结束符,或者当已读人的字符数比第第二个参数所指定的长度小1时,函数停止读取字符。内置数组的最后一个字符是留给结束空字符的。 如果遇到定界字符,则读取并丢弃它。
cin.getline
的第三个参数的默认值是'\n'
,因此,前面的函数调用可以写成下面的形式:
cin.getline(sentence,80);
显示C字符串
可以用cout
和<<
输出一个内置字符数组,该数组表示了一个以空终止符结束的字符串。下面的语句
cout << sentence;
显示内置数组sentence
。和cin
一样,cout
也不关心内置字符数组的大小。字符串中的字符会被输出,直到遇到终止符为止,空字符并不会输出。