《C++ Primer 4th》读书摘要
与 vector 类型相似,数组也可以保存某种类型的一组对象;而它们的区别在于,数组的长度是固定的。数组一经创建,就不允许添加新的元素。指针则可以像迭代器一样用于遍历和检查数组中的元素。设计良好的程序只有在强调速度时才在类实现的内部使用数组和指针。
此常量表达式只能包含整型字面值常量、枚举常量或者用常量表达式初始化的整型 const 对象。非 const 变量以及要到运行阶段才知道其值的const变量都不能用于定义数组的维数。
如果没有显式提供元素初值,则数组元素会像普通变量一样初始化。
在函数体外定义的内置数组,其元素均初始化为 0。
在函数体内定义的内置数组,其元素无初始化。
不管数组在哪里定义,如果其元素为类类型,则自动调用该类的默认构造函数进行初始化;如果该类没有默认构造函数,则必须为该数组的元素提供显式初始化。
显式初始化的数组不需要指定数组的维数值,编译器会根据列出的元素个数来确定数组的长度。
字符数组既可以用一组由花括号括起来、逗号隔开的字符字面值进行初始化,也可以用一个字符串字面值进行初始化。然而,要注意这两种初始化形式并不完全相同,字符串字面值包含一个额外的空字符(null)用于
结束字符串。
char ca1[] = {'C', '+', '+'}; // no null
char ca2[] = {'C', '+', '+', ' '}; // explicit null
char ca3[] = "C++"; // null terminator added automatically
一个数组不能用另外一个数组初始化,也不能将一个数组赋值给另一个数组。
数组下标的正确类型则是 size_t
解引用操作符 *(dereference operator)。& 符号是取地址操作符
理解指针声明语句时,请从右向左阅读。从右向左阅读 pstring 变量的定义,可以看到string *pstring;语句把 pstring 定义为一个指向 string 类型对象的指针变量。
在定义指针变量时,可用空格将符号 * 与其后的标识符分隔开来。下面的写法是合法的:
string* ps; // legal but can be misleading
一个有效的指针必然是以下三种状态之一:保存一个特定对象的地址;指向某个对象后面的另一对象;或者是0 值。若指针保存0 值,表明它不指向任何对象。未初始化的指针是无效的,直到给该指针赋值后,才可使用它。
int *pi = 0; // pi initialized to address no object
如果可能的话,除非所指向的对象已经存在,否则不要先定义指针,这样可避免定义一个未初始化的指针。
如果必须分开定义指针和其所指向的对象,则将指针初始化为 0。因为编译器可检测出 0 值的指针,程序可判断该指针并未指向一个对象。还可以使用 C++ 语言从 C 语言中继承下来的预处理器变量 NULL,该变量在 cstdlib
头文件中定义,其值为 0。
空指针表示"未分配"或者"尚未指向任何地方"。它与未初始化的指针不同,未初始化的指针可能指向任何地方。每种指针类型都要一个空指针,而不同类型的空指针的内部可能不尽相同。
C++ 提供了一种特殊的指针类型 void*,它可以保存任何类型对象的地址。void* 指针只支持几种有限的操作:与另一个指针进行比较;向函数传递void* 指针或从函数返回 void* 指针;给另一个 void* 指针赋值。不允许使用
void* 指针操纵它所指向的对象。
如果对左操作数进行解引用,则修改的是指针所指对象的值;如果没有使用解引用操作,则修改的是指针本身的值。
指针和引用的比较
第一个区别在于引用一经初始化,就始终指向同一个特定对象(实际上就是始终指向同一个内存地址,这就是为什么引用必须在定义时初始化的原因)。
第二个重要区别则是赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。
int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2; // pi now points to ival2
int &ri = ival, &ri2 = ival2;
ri = ri2; // assigns ival2 to ival
指针和数组密切相关。特别是在表达式中使用数组名时,该名字会自动转换为指向数组第一个元素的指针:
int ia[] = {0,2,4,6,8};
int *ip = ia; // ip points to ia[0]
两个指针减法操作的结果是标准库类型(library type)ptrdiff_t 的数据。在 cstddef 头文件中定义,是 signed 整型。
指针是数组的迭代器
如果指针指向const 对象,则不允许用指针来改变其所指的 const 值。为了保证这个特性,C++ 语言强制要求指向 const 对象的指针也必须具有 const 特性:
const double *cptr; // cptr may point to a double that is const
这里的 cptr 是一个指向 double 类型 const 对象的指针,const 限定了cptr 指针所指向的对象类型,而并非 cptr 本身
不能把一个 const 对象的地址赋给一个普通的、非 const 对象的指针。允许把非 const 对象的地址赋给指向 const 对象的指针。不能使用指向 const 对象的指针修改基础对象,无论该基础对象是const 对象或者非 const 对象。本质上来说,由于没有方法分辩 cptr 所指的对象是否为 const,系统会把它所指的所有对象都视为 const。
阅读 const 声明语句产生的部分问题,源于 const 限定符既可以放在类型前也可以放在类型后:
string const s1; // s1 and s2 have same type,
const string s2; // they're both strings that are const
字符串字面值的类型就是const char 类型的数组。
cstring 是 string.h 头文件的 C++ 版本,而 string.h 则是 C 语言提供的标准库。
操纵 C 风格字符串的标准库函数。传递给这些标准库函数例程的指针必须具有非零值,并且指向以 null 结束的字符数组中的第一个元素。传递给标准库函数 strcat 和 strcpy 的第一个实参数组必须具有足够大的空间存放新生成的字符串。
strlen(s) |
返回 s 的长度,不包括字符串结束符 null |
strcmp(s1, s2) |
比较两个字符串 s1 和 s2 是否相同。若 s1 与 s2 相等,返回 0;若 s1 大于 s2,返回正数;若 s1 小于 s2,则返回负数 |
strcat(s1, s2) |
将字符串 s2 连接到 s1 后,并返回 s1 |
strcpy(s1, s2) |
将 s2 复制给 s1,并返回 s1 |
strncat(s1,s2,n) |
将 s2 的前 n 个字符连接到 s1 后面,并返回 s1 |
strncpy(s1,s2, n) |
将 s2 的前 n 个字符复制给 s1,并返回 s1 |
C++ 语言提供普通的关系操作符实现标准库类型 string 的对象的比较。这些操作符也可用于比较指向C 风格字符串的指针,但效果却很不相同:实际上,此时比较的是指针上存放的地址值,而并非它们所指向的字符串:
if (cp1 < cp2) // compares addresses, not the values pointedto
C++ 标准库类型 string负责处理所有的内存管理问题,我们不必再担心每一次修改字符串时涉及到的大小问题。对大部分的应用而言,使用标准库类型 string,除了增强安全性外,效率也提高了,因此应该尽量避免使用 C 风格字符串。
虽然数组长度是固定的,但动态分配的数组不必在编译时知道其长度,可以(通常也是)在运行时才确定数组长度。与数组变量不同,动态分配的数组将一直存在,直到程序显式释放它为止。每一个程序在执行时都占用一块可用的内存空间,用于存放动态分配的对象,此内存空间称为程序的自由存储区或堆。C 语言程序使用一对标准库函数
malloc 和 free 在自由存储区中分配存储空间,而 C++ 语言则使用 new 和delete 表达式实现相同的功能。
动态分配数组时,只需指定类型和数组长度,不必为数组对象命名,new 表达式返回指向新分配数组的第一个元素的指针。在自由存储区中创建的数组对象是没有名字的,程序员只能通过其地址间接地访问堆中的对象:
int *pia = new int[10]; // array of 10 uninitialized ints
动态分配数组时,如果数组元素具有类类型,将使用该类的默认构造函数实现初始化;如果数组元素是内置类型,则无初始化:
string *psa = new string[10]; // array of 10 empty strings
int *pia = new int[10]; // array of 10 uninitialized ints
也可使用跟在数组长度后面的一对空圆括号,对数组元素做值初始化:
int *pia2 = new int[10] (); // array of 10 uninitialized ints
对于动态分配的数组,其元素只能初始化为元素类型的默认值,而不能像数组变量一样,用初始化列表为数组元素提供各不相同的初值。
之所以要动态分配数组,往往是由于编译时并不知道数组的长度。我们可以编写如下代码
size_t n = get_size(); // get_size returns number of elements needed
int* p = new int[n];
C++ 虽然不允许定义长度为 0 的数组变量,但明确指出,调用 new 动态创建长度为 0 的数组是合法的:
char arr[0]; // error: cannot define zero-length array
char *cp = new char[0]; // ok: but cp can't be dereferenced
动态分配的内存最后必须进行释放,否则,内存最终将会逐渐耗尽。如果不再需要使用动态创建的数组,程序员必须显式地将其占用的存储空间返还给程序的自由存储区。C++ 语言为指针提供 delete [] 表达式释放指针所指向的数组
空间:
delete [] pia;
在关键字 delete 和指针之间的空方括号对是必不可少的:它告诉编译器该指针指向的是自由存储区中的数组,而并非单个对象。
C++ 允许使用数组初始化 vector 对象,使用数组初始化 vector 对象,必须指出用于初始化式的第一个元素以及数组最后一个元素的下一位置的地址:
const size_t arr_size = 6;
int int_arr[arr_size] = {0, 1, 2, 3, 4, 5};
// ivec has 6 elements: each a copy of the corresponding elementin int_arr
vector<int> ivec(int_arr, int_arr + arr_size);
定义指向数组的指针与如何定义数组本身类似:首先声明元素类型,后接(数组)变量名字和维数。窍门在于(数组)变量的名字其实是指针,因此需在标识符前加上 *。如果从内向外阅读 ip 的声明,则可理解为:*ip 是 int[4] 类型——即 ip 是一个指向含有 4 个元素的数组的指针。
在下面的声明中,圆括号是必不可少的:
int *ip[4]; // array of pointers to int
int (*ip)[4]; // pointer to an array of 4 ints
typedef 类型定义可使指向多维数组元素的指针更容易读、写和理解。以下程序用 typedef 为 ia 的元素类型定义新的类型名:
typedef int int_array[4];
int_array *ip = ia;