日志:
1,2020-04-27 笔者提交文章的初版V1.0
作者按:
最近在学习C++ primer,初步打算把所学的记录下来。
C++ 语言提供了两种类似于 vector 和迭代器类型的低级复合类型——数组和指针。与 vector 类型相似,数组也可以保存某种类型的一组对象;
现代 C++ 程序应尽量使用 vector 和迭代器类型,而避免使用低级的数组和指针。设计良好的程序只有在强调速度时才在类实现的内部使用数组和指针。
数组
是 C++ 语言中类似于标准库 vector 类型的内置数据结构
。与 vector类似,数组也是一种存储单一数据类型对象的容器,其中每个对象都没有单独的名字,而是通过它在数组中的位置对它进行访问。
与 vector 类型相比,数组的显著缺陷
在于:
“缓冲区溢出(buffer overflow)”错误
:当我们在编程时没有检查下标,并且引用了越出数组或其他类似数据结构边界的元素时,就会导致这类错误。与使用标准 vector 类型的程序相比,依赖于内置数组的程序更容易出错而且难于调试。
在出现标准库之前,C++ 程序大量使用数组保存一组对象。而现代的 C++ 程序则更多地使用 vector 来取代数组,数组被严格限制于程序内部使用,只有当性能测试表明使用 vector 无法达到必要的速度要求时,才使用数组。然而,在将来一段时间之内,原来依赖于数组的程序仍大量存在,因此,C++ 程序员还是必须掌握数组的使用方法。
指针是用于数组的迭代器:指向数组中的一个元素。
解引用操作符 *(dereference operator)
和自增操作符 ++(increment operator)
,与在迭代器上的用法类似。复合数据类型
(书第 2.5 节)定义数组时,可为其元素提供一组用逗号分隔的初值,这些初值用花括号{}括起来,称为初始化列表
:
const unsigned array_size = 3;
int ia[array_size] = {0, 1, 2};
显式初始化的数组不需要指定数组的维数值,编译器会根据列出的元素个数来确定数组的长度:
int ia[] = {0, 1, 2}; // an array of dimension 3
如果指定了数组维数,那么初始化列表提供的元素个数不能超过维数值。
字符串字面值(第 2.2 节)包含一个额外的空字符(null)用于结束字符串。当使用字符串字面值来初始化创建的新数组时,将在新数组中加入空字符:
char ca1[] = {'C', '+', '+'}; // no null
char ca2[] = {'C', '+', '+', '\0'}; // explicit null
char ca3[] = "C++"; // null terminator added automatically 尾部自动加'\0';
复合数据类型
string s("hello world");
string *sp = &s; // sp holds the address of s
//*sp 中的 * 操作符表明 sp 是一个指针变量
注意事项
C++ 语言无法检测指针是否未被初始化,也无法区分有效地址和由指针分配到的存储空间中存放的二进制位形成的地址。建议程序员在使用之前初始化所有的变量,尤其是指针。如果可能的话,除非所指向的对象已经存在,否则不要先定义指针,这样可避免定义一个未初始化的指针。
上面&s 中的 & 符号是取地址操作符,当此操作符用于一个对象上时,返回的是该对象的存储地址。
取地址操作符只能用于左值
(第 2.3.1 节),因为只有当变量用作左值时,才能取其地址。 同样地,由于用于 vector 类型、string 类型或内置数组的下标操作和解引用操作生成左值,因此可对这两种操作的结果做取地址操作,这样即可获取某一特定对象的存储地址。
每个指针都有一个与之关联的数据类型,该数据类型决定了指针所指向的对象的类型。例如,一个 int 型指针只能指向 int 型对象:
C++ 语言使用*
符号把一个标识符声明为指针:
vector<int> *pvec; // pvec can point to a vector
int *ip1, *ip2; // ip1 and ip2 can point to an int
string *pstring; // 把 pstring 定义为一个指向 string 类型对象的指针变量。
double *dp; // dp can point to a double
理解指针声明语句时,请从右向左阅读。
double dp, *dp2; // dp2 is a ponter, dp is an object: both type double
该语句定义了一个 double 类型的 dp 对象以及一个指向 double 类型对象的指针 dp2。
如果需要在一个声明语句中定义两个指针,必须在每个变量标识符前 再加符号 * 声明:
string *ps1, *ps2; // both ps1 and ps2 are pointers to string
//注意不要把string * 误认为是一种数据类型,二者要分开看
有效的指针必然是以下三种状态之一:
对指针进行初始化或赋值只能使用以下四种类型的值:
除了将在第 4.2.5 节和第 15.3 节介绍的两种例外情况之外,指针只能初
始化或赋值为同类型的变量地址或另一指针:
double dval;
double *pd = &dval; // ok: initializer is address of a double
double *pd2 = pd; // ok: initializer is a pointer to double
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
void* 表明该指针与一地址值相关,但不清楚存储在此地址上的对象的类型。
void* 指针只支持几种有限的操作:
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
//因为 sp 指向 s,所以给 *sp 赋值也就修改了 s 的值。
//修改指针 sp 本身的值,使 sp 指向另外一个新对象:
string s2 = "some value";
sp = &s2; // sp now points to s2
虽然使用引用(reference)和指针都可间接访问另一个值,但它们之间有两个重要区别:
考虑以下两个程序段。第一个程序段将一个指针赋给另一指针:
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 对象,而并非引用本身。赋值后,这两个引用还是分别指向原来关联的对象,此时这两个对象的值相等。
指针和引用的相同点:
指针和引用的不同点:
指针是一个实体,而引用仅是个别名;
引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
引用没有const,指针有const,const的指针不可变;(具体指没有int& const a这种形式,而const int& a是有的, 前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
引用不能为空,指针可以为空;
“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
指针和引用的自增(++)运算意义不一样;
引用是类型安全
的,而指针不是 (引用比指针多了类型检查)
传指针和传指针引用的区别/指针和引用的区别(本质)