数组
数组定义中的类型名可以是内置数据类型或类类型;除引用之外,数组元素的类型还可以是任意的复合类型。没有所有元素都是引用的数组。
除非显式地提供元素初值,否则内置类型的局部数组的元素没有初始化。此时,除了给定元素之外,其他使用这些元素的操作没有定义。
不允许数组直接复制和赋值
int ia2[](ia); // error: cannot initialize one array with another
int main()
{
const unsigned array_size = 3 ;
int ia3[array_size]; // ok: but elements are uninitialized!
ia3 = ia; // error: cannot assign one array to another
return 0 ;
}
警告:数组的长度是固定的
与vector类型不同,数组不提供push_back或者其他的操作在数组中添加新元素,数组一经定义,就不允许再添加新元素。
如果必须在数组中添加新元素,程序员就必须自己管理内存:要求系统重新分配一个新的内存空间用于存放更大的数组,然后把原数组的所有元素复制到新分配的内存空间中。
导致安全问题的最常见原因是所谓“缓冲区溢出(buffer overflow)”错误。当我们在编程时没有检查下标,并且引用了越出数组或其他类似数据结构边界的元素时,就会导致这类错误。
指针的引入
建议:尽量避免使用指针和数组
指针和数组容易产生不可预料的错误。其中一部分是概念上的问题:指针用于低级操作,容易然生与繁琐细节相关的(book keeping)错误。其他错误则源于使用指针的语法规则,特别是声明指针的语法。
许多有用的程序都可不使用数组或指针实现,现代C++程序采用vector类型和迭代器取代一般的数组、采用string类型取代C风格字符串。
指针可能的取值
一个有效的指针必然是以下三种状态之一:保存一个特定对象的地址;指向某个对象后面的另一对象;或者是0值。若指针保存0值,表明它不指向任何对象。未初始化的指针是无效的,直到给该指针赋值后,才可使用它。
int * pi = 0 ; // pi initialized to address no object
int * pi2 = & ival; // pi2 initialized to address of ival
int * pi3; // ok, but dangerous, pi3 is uninitialized
pi = pi2; // pi and pi2 address the same object, e.g. ival
pi2 = 0 ; // pi2 now addresses no object
避免使用未初始化的指针
很多运行时错误都源于使用了未初始化的指针。
如果可能的话,除非所指向的对象已经存在,否则不要先定义指针,这样可避免定义一个未初始化的指针。
如果必须分开定义指针和其所指向的对象,则将指针初始化为0.因为编译器可检测出0值的指针,程序可判断该指针并未指向一个对象。
指针初始化和赋值操作的约束
对指针进行初始化或赋值只能使用以下四种类型的值:
(1)0值常量表达式。
(2)类型匹配的对象的地址。
(3)另一对象之后的下一地址。
(4)同类型的另一个有效指针。
把int型变量赋给指针是非法的,尽管此int型变量的值可能为0。
void*指针
C++提供了一种特殊的指针类型void*,它可以保存任何类型对象的地址:
double * pd = & obj;
// ok: void* can hold the address value of any data pointer type
void * pv = & obj; // obj can be an object of any type
pv = pd; // pd can be a pointer to any type
void*表明该指针与一地址值相关,但不清楚存储在此地址上的对象的类型。
void*指针只支持几种有限的操作:与另一个指针进行比较;向函数传递void*指针或从函数返回void*指针;给另一个void*指针复制。不允许用void*指针操纵它所指向的对象。
指针操作
解引用操作生成左值
关键概念:给指针赋值或通过指针进行赋值
对于初学指针者,给指针赋值和通过指针进行赋值这两种操作的差别确实让人费解。谨记区分的重要方法是:如果对左操作数进行解引用,则修改的是指针所指向的值;如果没有使用解引用操作,则修改的是指针本身的值。
指针和引用的比较
第一个区别在于引用总是指向某个对象:定义引用时没有初始化是错误的。第二个重要区别则是复制行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。引用一经初始化,就始终指向同一个特定对象(这就是为什么引用必须在定义时初始化的原因)。
指向指针的指针
指针本身也是可用指针指向的内存对象。指针占用内存空间存放其值,因此指针的存储地址可存放在指针中。
int * ip = ia; // ip points to ia[0]
ip = & ia[ 4 ]; // ip points to last element in ia
ip = ia; // ok: ip points to ia[0]
int * ip2 = ip + 4 ; // ok: ip2 points to ia[4], the last element in ia
指针的算数操作只有在原指针和计算出来的新指针都指向同一个数组的元素,或指向该数组存储空间的下一单元时才是合法的。如果指针指向一对象,我们还可以在指针上加1从而获取指向相邻的下一个对象的指针。
C++还支持对这两个指针做减法操作:
结果是4,这两个指针所指向的元素间隔为4个对象。两个指针减法操作的结果是标准库类型ptrdiff_t的数据。与size_t类型一样,ptrdiff_t也是一种与机器相关的类型,在cstddef头文件中定义。size_t是unsigned类型,而ptrdiff_t则是signed_t整型。
允许在指针上加减0,使指针保持不变。如果一指针具有0值,则在该指针上加0仍然是合法的,结果得到另一个值为0的指针。也可以对两个空指针做减法操作,得到的结果仍是0。
解引用和指针算术操作之间的相互作用
在指针上加一个整型数值,其结果仍然是指针。允许在这个结果上直接进行解引用操作,而不必先把它赋给一个新指针:
加法操作两边用圆括号括起来是必要的。如果写为:
意味着对ia进行解引用,获得ia所指元素的值ia[0],然后加4。
计算数组的超出末端指针
int arr[arr_size] = { 1 , 2 , 3 , 4 , 5 };
int * p = arr; // ok: p points to arr[0]
int * p2 = p + arr_size; // ok: p2 points one past the end of arr
// use caution -- do not dereference!
C++允许计算数组或对象的超出末端的地址,但不允许对此地址进行解引用操作。而计算数组超出末端位置之后或数组首地址之前的地址都是不合法的。
指针和const限定符
指向const对象的指针
const限定了cptr指针所指向的对象类型,而并非cptr本身。也就是说,cptr本身并不是const。
不能使用void*指针保存const对象的地址,而必须使用const void*类型的指针保存const对象的地址:
const void * cpv = & universe; // ok: cpv is const
void * pv = & universe; // error: universe is const
不能使用指向const对象的指针修改基础对象,然而如果该指针指向的是一个非const对象,可用其他方法修改其所指的对象。
const指针
C++语言还提供了const指针——本身的值不能修改:
int * const curErr = & errNumb; // curErr is a constant pointer
curErr = curErr; // error: curErr is a constant pointer
指向const对象的const指针
既不能修改所指对象的值,也不允许修改指针的指向。
指针和typedef
假设给出以下语句:
const pstring cstr;
请问cstr变量是什么类型?
string * const cstr; // equivalent to const pstring cstr
C风格字符串
C风格字符串的标准库函数(要使用这些标准库函数,必须包含相应的C头文件:cstring)
strcpy(s1, s2) strncat(s1, s2, n) strncpy(s1, s2, n)
注意:这些标准库函数不会检查其字符串参数。
永远不要忘记字符串结束符null
调用者必须确保目标字符串具有足够的大小
如果必须使用C风格字符串,则使用标准库函数strncat和strncpy比strcat和strcpy函数更安全:
strncpy(largeStr, cp1, 17 ); // size to copy includes the null
strncat(largeStr, " " , 2 ); // pedantic, but a good habit
strncat(largeStr, cp2, 19 ); // adds at most 18 characters, plus a null
对大部分的应用而言,使用标准库类型string,除了增强安全性外,效率也提高了,因此应该尽量避免使用C风格字符串。
创建动态数组
动态数组的定义
new表达式返回指向新分配数组的第一个元素的指针。
初始化动态分配的数组
可使用跟在数组长度后面的一对空圆括号,对数组元素做值初始化:
对于动态分配的数组,其元素只能初始化为元素类型的默认值,而不能像数组变量一样,用初始化列表为数组元素提供各不相同的初值。
const对象的动态数组
const int * pci_bad = new const int [ 100 ];
// ok: value-initialized const array
const string * pci_ok = new const int [ 100 ]();
允许动态分配空数组
char * cp = new char [ 0 ]; // ok: but cp can't be dereferenced
用new动态创建长度为0的数组时,new返回有效的非零指针。该指针与new返回的其他指针不同,不能进行解引用操作,因为它毕竟没有指向任何元素。而允许的操作包括:比较运算,因此该指针能在循环中使用;在该指针上加(减)0;或者减去本身,得0值。
动态空间的释放
动态分配的内存最后必须进行释放,否则,内存最终将会逐渐耗尽。
新旧代码的兼容
混合使用标准库类string和C风格字符串
char * str = st2; // compile-time type error
char * str = st2.c_str(); // almost ok, but not quite
const char * str = st2.c_str(); // ok
c_str返回的指针指向const char类型的数组。
使用数组初始化vector对象
int int_arr[arr_size] = { 0 , 1 , 2 , 3 , 4 , 5 };
// ivec has 6 elements: each a copy of the corresponding element in int_arr
vector < int > ivcec(int_arr, int_arr + arr_size);