首页 > 代码库 > C++primer第四章 数组和指针

C++primer第四章 数组和指针

4.1. 数组

  数组是由类型名标识符维数组成的复合数据类型(第 2.5 节),类型名规定了存放在数组中的元素的类型,而维数则指定数组中包含的元素个数

  数组定义中的类型名可以是内置数据类型类类型除引用之外,数组元素的类型还可以是任意的复合类型。没有所有元素都是引用的数组。

4.1.1. 数组的定义和初始化

  数组的维数必须用值大于等于 1 的常量表达式定义(第 2.7 节)。

  此常量表达式只能包含整型字面值常量枚举常量(第 2.7 节)或者用常量表达式初始化的整型 const 对象

// both buf_size and max_files are constconst unsigned buf_size = 512, max_files = 20;int staff_size = 27; // nonconstconst unsigned sz = get_size(); // const value not known until run timechar input_buffer[buf_size]; // ok: const variablestring fileTable[max_files + 1]; // ok: constant expressiondouble salaries[staff_size]; // error: non const variableint test_scores[get_size()]; // error: non const expressionint 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 0int ia[array_size] = {0, 1, 2};// Equivalent to str_arr = {"hi", "bye", "", "", ""}// str_arr[2] through str_arr[4] default initialized to the empty stringstring str_arr[array_size] = {"hi", "bye"};

特殊的字符数组

  字符数组既可以用一组由花括号括起来逗号隔开的字符字面值进行初始化,也可以用一个字符串字面值进行初始化。

char ca1[] = {C, +, +}; // no nullchar ca2[] = {C, +, +, \0}; // explicit nullchar ca3[] = "C++"; // null terminator added automatically

  不允许数组直接复制和赋值

int ia[] = {0, 1, 2}; // ok: array of intsint ia2[](ia); // error: cannot initialize one arraywith anotherint 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 intstring *pstring; // pstring can point to a stringdouble *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 objectint *pi2 = & ival; // pi2 initialized to address of ivalint *pi3; // ok, but dangerous, pi3 is uninitializedpi = pi2; // pi and pi2 address the same object, e.g.ivalpi2 = 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 0int *pi = NULL; // ok: equivalent to int *pi = 0;

  指针只能初始化或赋值为同类型的变量地址或另一指

double dval;double *pd = &dval; // ok: initializer is address of a doubledouble *pd2 = pd; // ok: initializer is a pointer to doubleint *pi = pd; // error: types of pi and pd differpi = &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 typevoid *pv = &obj; // obj can be an object of any typepv = 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 scout <<*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 intint **ppi = &pi; // 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 by2int 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 notdereference!

输出数组元素

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 thelastfor (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 = &pi; // error: ptr is a plain pointerconst double *cptr = &pi; // 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 constvoid *pv = &universe; // error: universe is const

   允许把非 const 对象的地址赋给指向 const 对象的指针,例如:

double dval = 3.14; // dval is a double; its value can be changedcptr = &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 constdouble *ptr = &dval; // ok: ptr points at non-const double*ptr = 2.72; // ok: ptr is plain pointercout << *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 objectconst double *const pi_ptr = &pi;

指针和 typedef(太晕了,暂时不讲了)

 4.3. C 风格字符串

   现在可以更明确地认识到:字符串字面值的类型就是const char 类型的数组。

char ca1[] = {C, +, +}; // no null, not C-style stringchar ca2[] = {C, +, +, \0}; // explicit nullchar ca3[] = "C++"; // null terminator added automaticallyconst char *cp = "C++"; // null terminator added automaticallychar *cp1 = ca1; // points to first element of a array, but not C-style stringchar *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-terminatedcout << strlen(ca) << endl; // disaster: ca isn‘t null-terminated

调用者必须确保目标字符串具有足够的大小

  传递给标准库函数 strcat 和 strcpy 的第一个实参数组必须具有足够大的空间存放新生成的字符串。以下代码虽然演示了一种通常的用法,但是却有潜在的严重错误:

// Dangerous: What happens if we miscalculate the size of largeStrchar largeStr[16 + 18 + 2]; // will hold cp1 a spaceand cp2strcpy(largeStr, cp1); // copies cp1 into largeStrstrcat(largeStr, " "); // adds a space at end of largeStrstrcat(largeStr, cp2); // concatenates cp2 to largeStr// prints A string example A different stringcout << largeStr << endl;

使用strn 函数处理 C 风格字符串

char largeStr[16 + 18 + 2]; // to hold cp1 a space and cp2strncpy(largeStr, cp1, 17); // size to copy includes the nullstrncat(largeStr, " ", 2); // pedantic, but a good habitstrncat(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 cp1largeStr += " "; // add space at end of largeStrlargeStr += 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 4int 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 rowint 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 rowfor (size_t i = 0; i != rowSize; ++i)// for each column within the rowfor (size_t j = 0; j != colSize; ++j)// initialize to its positional indexia[i][j] = i * colSize + j;

4.4.1. 指针和多维数组

int ia[3][4]; // array of size 3, each element is an array ofints of size 4int (*ip)[4] = ia; // ip points to an array of 4 intsip = &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 风格字符串。

 

 

 

 

C++primer第四章 数组和指针