4.1. 数组
数组是由类型名、标识符和维数组成的复合数据类型(第 2.5 节),类型名规定了存放在数组中的元素的类型,而维数则指定数组中包含的元素个数。
数组定义中的类型名可以是内置数据类型或类类型;除引用之外,数组元素的类型还可以是任意的复合类型。没有所有元素都是引用的数组。
4.1.1. 数组的定义和初始化
数组的维数必须用值大于等于 1 的常量表达式定义(第 2.7 节)。
此常量表达式只能包含整型字面值常量、枚举常量(第 2.7 节)或者用常量表达式初始化的整型 const 对象。
// both buf_size and max_files are const const unsigned buf_size = 512, max_files = 20; int staff_size = 27; // nonconst const unsigned sz = get_size(); // const value not known until run time char input_buffer[buf_size]; // ok: const variable string fileTable[max_files + 1]; // ok: constant expression double salaries[staff_size]; // error: non const variable int test_scores[get_size()]; // error: non const expression int vals[sz]; // error: size not knownuntil run time
显式初始化数组元素
在定义数组时,可为其元素提供一组用逗号分隔的初值,这些初值用花括号{}括起来,称为初始化列表:
const unsigned array_size = 3; int ia[array_size] = {0, 1, 2};
如果没有显式提供元素初值,则数组元素会像普通变量一样初始化(第 2.3.4节):
• 在函数体外定义的内置数组,其元素均初始化为 0。
• 在函数体内定义的内置数组,其元素无初始化。
• 不管数组在哪里定义,如果其元素为类类型,则自动调用该类的默认构造
函数进行初始化;如果该类没有默认构造函数,则必须为该数组的元素提供显式初始化。
显式初始化的数组不需要指定数组的维数值,编译器会根据列出的元素个数来确定数组的长度:
int ia[] = {0, 1, 2}; // an array of dimension 3
如果指定了数组维数,那么初始化列表提供的元素个数不能超过维数值。如果维数大于列出的元素初值个数,则只初始化前面的数组元素;剩下的其他元素,若是内置类型则初始化为0,若是类类型则调用该类的默认构造函数进行初始化:
const unsigned array_size = 5; // Equivalent to ia = {0, 1, 2, 0, 0} // ia[3] and ia[4] default initialized to 0 int ia[array_size] = {0, 1, 2}; // Equivalent to str_arr = {"hi", "bye", "", "", ""} // str_arr[2] through str_arr[4] default initialized to the empty string string str_arr[array_size] = {"hi", "bye"};
特殊的字符数组
字符数组既可以用一组由花括号括起来、逗号隔开的字符字面值进行初始化,也可以用一个字符串字面值进行初始化。
char ca1[] = {‘C‘, ‘+‘, ‘+‘}; // no null char ca2[] = {‘C‘, ‘+‘, ‘+‘, ‘\0‘}; // explicit null char ca3[] = "C++"; // null terminator added automatically
不允许数组直接复制和赋值
int ia[] = {0, 1, 2}; // ok: array of ints int ia2[](ia); // error: cannot initialize one arraywith another int main() { const unsigned array_size = 3; int ia3[array_size]; // ok: but elements are uninitialized! ia3 = ia; // error: cannot assign one array toanother return 0; }
4.1.2. 数组操作
int main() { const size_t array_size = 10; int ia[array_size]; // 10 ints, elements are uninitialized // loop through array, assigning value of its index to each element for (size_t ix = 0; ix != array_size; ++ix) ia[ix] = ix; return 0; }
使用类似的循环,可以实现把一个数组复制给另一个数组?
int main() { const size_t array_size = 7; int ia1[] = { 0, 1, 2, 3, 4, 5, 6 }; int ia2[array_size]; // local array, elementsuninitialized // copy elements from ia1 into ia2 for (size_t ix = 0; ix != array_size; ++ix) ia2[ix] = ia1[ix]; return 0; }
检查数组下标值
导致安全问题的最常见原因是所谓“缓冲区溢出(buffer overflow)”错误。当我们在编程时没有检查下标,并且引用了越出数组或其他类似数据结构边界的元素时,就会导致这类错误。
4.2. 指针的引入
4.2.1. 什么是指针
指针的概念很简单:指针用于指向对象。
具体来说,指针保存的是另一个对象的地址:
string s("hello world"); string *sp = &s; // sp holds the address of s
第二条语句定义了一个指向 string 类型的指针 sp,并初始化 sp 使其指向 string 类型的对象s。*sp 中的 * 操作符表明 sp 是一个指针变量,&s 中的 & 符号是取地址操作符,当此操作符用于一个对象上时,返回的是该对象的存储地址。取地址操作符只能用于左值(第 2.3.1 节),因为只有当变量用作左值时,才能取其地址。同样地,由于用于 vector 类型、string 类型或内置数组的下标操作和解引用操作生成左值,因此可对这两种操作的结果做取地址操作,这样即可获取某一特定对象的存储地址。
4.2.2. 指针的定义和初始化
指针变量的定义
C++ 语言使用 * 符号把一个标识符声明为指针:
vector<int> *pvec; // pvec can point to a vector<int> int *ip1, *ip2; // ip1 and ip2 can point to an int string *pstring; // pstring can point to a string double *dp; // dp can point to a double
另一种声明指针的风格
string* ps; // legal but can be misleading
连续声明多个指针易导致混淆
string* ps1,ps2;
指针可能的取值
一个有效的指针必然是以下三种状态之一:
- 保存一个特定对象的地址;
- 指向某个对象后面的另一对象;
- 或者是0 值。
int ival = 1024; 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
指针初始化和赋值操作的约束
对指针进行初始化或赋值只能使用以下四种类型的值
1. 0 值常量表达式(第 2.7 节),例如,在编译时可获得 0 值的整型 const对象或字面值常量 0。
2. 类型匹配的对象的地址。
3. 另一对象末的下一地址。
4. 同类型的另一个有效指针
除了使用数值0 或在编译时值为 0 的 const 量外,还可以使用 C++ 语言从 C 语言中继承下来的预处理器变量 NULL(第 2.9.2 节),该变量在 cstdlib头文件中定义,其值为 0。如果在代码中使用了这个预处理器变量,则编译时会自动被数值 0 替换。因此,把指针初始化为 NULL 等效于初始化为 0 值:
// cstdlib #defines NULL to 0 int *pi = NULL; // ok: equivalent to int *pi = 0;
指针只能初始化或赋值为同类型的变量地址或另一指
double dval; double *pd = &dval; // ok: initializer is address of a double double *pd2 = pd; // ok: initializer is a pointer to double int *pi = pd; // error: types of pi and pd differ pi = &dval; // error: attempt to assign address of a doubleto int *
void* 指针
C++ 提供了一种特殊的指针类型 void*,它可以保存任何类型对象的地址:
double obj = 3.14; 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
4.2.3. 指针操作
与对迭代器进行解引用操作(第 3.4节)一样,对指针进行解引用可访问它所指的对象,* 操作符(解引用操作符)将获取指针所指的对象:
string s("hello world"); string *sp = &s; // sp holds the address of s cout <<*sp; // prints hello world
生成左值的解引用操作
解引用操作符返回指定对象的左值,利用这个功能可修改指针所指对象的值:
*sp = "goodbye"; // contents of s now changed
指针和引用的比较
第一个区别在于引用总是指向某个对象:定义引用时没有初始化是错误的。
第二个重要区别则是赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。
考虑以下两个程序段。第一个程序段将一个指针赋给另一指针:
int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = &ival2; pi = pi2; // pi now points to ival2
赋值结束后,pi 所指向的 ival 对象值保持不变,赋值操作修改了 pi 指针的值,使其指向另一个不同的对象。
现在考虑另一段相似的程序,使用两个引用赋值:
int &ri = ival, &ri2 = ival2; ri = ri2; // assigns ival2 to ival
这个赋值操作修改了 ri 引用的值 ival 对象,而并非引用本身。赋值后,这两个引用还是分别指向原来关联的对象,此时这两个对象的值相等。
指向指针的指针
指针本身也是可用指针指向的内存对象。指针占用内存空间存放其值,因此指针的存储地址可存放在指针中。
int ival = 1024; int *pi = &ival; // pi points to an int int **ppi = π // ppi points to a pointer to int
定义了指向指针的指针。C++ 使用 ** 操作符指派一个指针指向另一指针。
这些对象可表示为:
为了真正地访问到 ival 对象,必须对 ppi 进行两次解引用:
cout << "The value of ival\n" << "direct value: " << ival << "\n" << "indirect value: " << *pi << "\n" << "doubly indirect value: " << **ppi << endl;
这段程序用三种不同的方式输出 ival 的值。首先,采用直接引用变量的方式输出;然后使用指向 int 型对象的指针 pi 输出;最后,通过对 ppi 进行两次解引用获得 ival 的特定值。
4.2.4. 使用指针访问数组元素
int ia[] = {0,2,4,6,8}; 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 elementin ia
只要两个指针指向同一数组或有一个指向该数组末端的下一单元,C++ 还支持对这两个指针做减法操作:
ptrdiff_t n = ip2 - ip; // ok: distance between the pointers
解引用和指针算术操作之间的相互作用
在指针上加一个整型数值,其结果仍然是指针。允许在这个结果上直接进行解引用操作,而不必先把它赋给一个新指针:
int last = *(ia + 4); // ok: initializes last to 8, the valueof ia[4]
加法操作两边用圆括号括起来是必要的。如果写为:
last = *ia + 4; // ok: last = 4, equivalent to ia[0]+4
意味着对 ia 进行解引用,获得 ia 所指元素的值 ia[0],然后加 4。
下标和指针
在表达式中使用数组名时,实际上使用的是指向数组第一个元素的指针。
int ia[] = {0,2,4,6,8}; int i = ia[0]; // ia points to the first element in ia
int *p = &ia[2]; // ok: p points to the element indexed by2 int j = p[1]; // ok: p[1] equivalent to *(p + 1), // p[1] is the same element as ia[3] int k = p[-2]; // ok: p[-2] is the same element as ia[0]
在使用下标访问数组时,实际上是对指向数组元素的指针做下标操作。
计算数组的超出末端指针
可以计算数组的超出末端指针的值:
const size_t arr_size = 5; 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 ofarr // use caution -- do not dereference!
输出数组元素
const size_t arr_sz = 5; int int_arr[arr_sz] = { 0, 1, 2, 3, 4 }; // pbegin points to first element, pend points just after thelast for (int *pbegin = int_arr, *pend = int_arr + arr_sz; pbegin != pend; ++pbegin) cout << *pbegin << ‘ ‘; // print the current element
4.2.5. 指针和const 限定符
指向const 对象的指针
到目前为止,我们使用指针来修改其所指对象的值。但是如果指针指向const 对象,则不允许用指针来改变其所指的 const 值。为了保证这个特性,C++ 语言强制要求指向 const 对象的指针也必须具有 const 特性:
const double *cptr; // cptr may point to a double that is const
把一个 const 对象的地址赋给一个普通的、非 const 对象的指针也会导致编译时的错误:
const double pi = 3.14; double *ptr = π // error: ptr is a plain pointer const double *cptr = π // ok: cptr is a pointer to const
不能使用 void* 指针(第 4.2.2 节)保存 const 对象的地址,而必须使用 const void* 类型的指针保存 const 对象的地址:
const int universe = 42; const void *cpv = &universe; // ok: cpv is const void *pv = &universe; // error: universe is const
允许把非 const 对象的地址赋给指向 const 对象的指针,例如:
double dval = 3.14; // dval is a double; its value can be changed cptr = &dval; // ok: but can‘t change dval through cptr
******************注意**************************
dval = 3.14159; // dval is not const *cptr = 3.14159; // error: cptr is a pointer to const double *ptr = &dval; // ok: ptr points at non-const double *ptr = 2.72; // ok: ptr is plain pointer cout << *cptr; // ok: prints 2.72
如果指向 const 的指针所指的对象并非 const,则可直接给该对象赋值或间接地利用普通的非 const 指针修改其值:毕竟这个值不是 const。重要的是要记住:不能保证指向 const 的指针所指对象的值一定不可修改。
const 指针
与上边的指向const数据的指针对比,找区别
int errNumb = 0; int *const curErr = &errNumb; // curErr is a constant pointer
“curErr 是指向 int 型对象的const 指针”。
指向const 对象的 const 指针
这段代码什么意思?
const double pi = 3.14159; // pi_ptr is const and points to a const object const double *const pi_ptr = π
指针和 typedef(太晕了,暂时不讲了)
4.3. C 风格字符串
现在可以更明确地认识到:字符串字面值的类型就是const char 类型的数组。
char ca1[] = {‘C‘, ‘+‘, ‘+‘}; // no null, not C-style string char ca2[] = {‘C‘, ‘+‘, ‘+‘, ‘\0‘}; // explicit null char ca3[] = "C++"; // null terminator added automatically const char *cp = "C++"; // null terminator added automatically char *cp1 = ca1; // points to first element of a array, but not C-style string char *cp2 = ca2; // points to first element of a null-terminated char array
C 风格字符串的使用
const char *cp = "some value"; while (*cp) { // do something to *cp ++cp; }
C 风格字符串的标准库函数
cstring 是 string.h 头文件的 C++ 版本,而 string.h 则是 C 语言提供的标准库。
永远不要忘记字符串结束符 null
char ca[] = {‘C‘, ‘+‘, ‘+‘}; // not null-terminated cout << strlen(ca) << endl; // disaster: ca isn‘t null-terminated
调用者必须确保目标字符串具有足够的大小
传递给标准库函数 strcat 和 strcpy 的第一个实参数组必须具有足够大的空间存放新生成的字符串。以下代码虽然演示了一种通常的用法,但是却有潜在的严重错误:
// Dangerous: What happens if we miscalculate the size of largeStr char largeStr[16 + 18 + 2]; // will hold cp1 a spaceand cp2 strcpy(largeStr, cp1); // copies cp1 into largeStr strcat(largeStr, " "); // adds a space at end of largeStr strcat(largeStr, cp2); // concatenates cp2 to largeStr // prints A string example A different string cout << largeStr << endl;
使用strn 函数处理 C 风格字符串
char largeStr[16 + 18 + 2]; // to hold cp1 a space and cp2 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
• 调用 strncpy 时,要求复制 17 个字符:字符串 cp1 中所有字符,加上结束符 null。留下存储结束符 null 的空间是必要的,这样 largeStr 才可以正确地结束。调用 strncpy 后,字符串 largeStr 的长度 strlen 值是 16。记住:标准库函数 strlen 用于计算 C 风格字符串中的字符个数,不包括 null 结束符。
• 调用 strncat 时,要求复制 2 个字符:一个空格和结束该字符串字面值的 null。调用结束后,字符串 largeStr 的长度是 17,原来用于结束largeStr 的 null 被新添加的空格覆盖了,然后在空格后面写入新的结束符 null。
• 第二次调用 strncat 串接 cp2 时,要求复制 cp2 中所有字符,包括字符串结束符 null。调用结束后,字符串 largeStr 的长度是 35:cp1 的16 个字符和 cp2 的 18 个字符,再加上分隔这两个字符串的一个空格。
尽可能使用标准库类型string
string largeStr = cp1; // initialize large Str as a copy of cp1 largeStr += " "; // add space at end of largeStr largeStr += cp2; // concatenate cp2 onto end of largeStr
4.3.1. 创建动态数组
4.4. 多维数组
// array of size 3, each element is an array of ints of size 4 int ia[3][4];
多维数组的初始化
int ia[3][4] = { /* 3 elements, each element is an array of size 4 */ {0, 1, 2, 3} , /* initializers for row indexed by 0 */ {4, 5, 6, 7} , /* initializers for row indexed by 1 */ {8, 9, 10, 11} /* initializers for row indexed by 2 */ };
// equivalent initialization without the optional nested braces foreach row int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
多维数组的下标引用
const size_t rowSize = 3; const size_t colSize = 4; int ia [rowSize][colSize]; // 12 uninitialized elements // for each row for (size_t i = 0; i != rowSize; ++i) // for each column within the row for (size_t j = 0; j != colSize; ++j) // initialize to its positional index ia[i][j] = i * colSize + j;
4.4.1. 指针和多维数组
int ia[3][4]; // array of size 3, each element is an array of ints of size 4 int (*ip)[4] = ia; // ip points to an array of 4 ints ip = &ia[2]; // ia[2] is an array of 4 ints
用 typedef 简化指向多维数组的指针
typedef int int_array[4]; int_array *ip = ia;
可使用 typedef 类型输出 ia 的元素
for (int_array *p = ia; p != ia + 3; ++p) for (int *q = *p; q != *p + 4; ++q) cout << *q << endl;
小结
本章介绍了数组和指针。数组和指针所提供的功能类似于标准库的 vector类与 string 类和相关的迭代器所提供。我们可以把 vector 类型理解为更灵活、更容易管理的数组,同样,string 是 C 风格字符串的改进类型,而 C 风格字符串是以空字符结束的字符数组。
迭代器和指针都能用于间接地访问所指向的对象。vector 类型所包含的元素通过迭代器来操纵,类似地,指针则用于访问数组元素。尽管道理都很简单,但在实际应用中,指针的难用是出了名的。
某些低级任务必须使用指针和数组,但由于使用指针和数组容易出错而且难以调试,应尽量避免使用。一般而言,应该优先使用标准库抽象类而少用语言内置的低级数组和指针。尤其是应该使用 string 类型取代 C 风格以空字符结束的字符数组。现代 C++ 程序不应使用C 风格字符串。