《C++Primer》第四章-数组和指针-学习笔记(1)-数组指针引用

《C++Primer》第四章-数组和指针-学习笔记(1)-数组指针引用

文章目录

  • 《C++Primer》第四章-数组和指针-学习笔记(1)-数组指针引用
    • 概述
    • 指针与数组之间的关系
    • 数组
      • 数组的定义和初始化
        • 显式初始化数组元素
        • 特殊的字符数组
    • 指针
      • 指针是什么
      • 取地址操作符
      • 指针的定义和初始化
        • 指针变量的定义
        • 指针可能的取值
        • 指针初始化和赋值操作的约束
      • void* 指针
      • **指针操作**
      • 指针和引用的比较
      • 向函数中传递指针和传递引用的区别
      • 向函数中传递指针的引用
    • 参考资料

日志:
1,2020-04-27 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

概述

C++ 语言提供了两种类似于 vector 和迭代器类型的低级复合类型——数组和指针。与 vector 类型相似,数组也可以保存某种类型的一组对象;

  • 数组的长度是固定的。数组一经创建,就不允许添加新的元素。
  • 指针则可以像迭代器一样用于遍历和检查数组中的元素。

现代 C++ 程序应尽量使用 vector 和迭代器类型,而避免使用低级的数组和指针。设计良好的程序只有在强调速度时才在类实现的内部使用数组和指针。

数组是 C++ 语言中类似于标准库 vector 类型的内置数据结构。与 vector类似,数组也是一种存储单一数据类型对象的容器,其中每个对象都没有单独的名字,而是通过它在数组中的位置对它进行访问。
与 vector 类型相比,数组的显著缺陷在于:

  • 数组的长度是固定的,而且也不提供 push_back 操作在其中自动添加元素。
  • 数组没有获取其容量大小的 size 操作,
  • 如果需要更改数组的长度,程序员只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组空间中去。
  • “缓冲区溢出(buffer overflow)”错误:当我们在编程时没有检查下标,并且引用了越出数组或其他类似数据结构边界的元素时,就会导致这类错误。

与使用标准 vector 类型的程序相比,依赖于内置数组的程序更容易出错而且难于调试。
在出现标准库之前,C++ 程序大量使用数组保存一组对象。而现代的 C++ 程序则更多地使用 vector 来取代数组,数组被严格限制于程序内部使用,只有当性能测试表明使用 vector 无法达到必要的速度要求时,才使用数组。然而,在将来一段时间之内,原来依赖于数组的程序仍大量存在,因此,C++ 程序员还是必须掌握数组的使用方法。

指针与数组之间的关系

指针是用于数组的迭代器:指向数组中的一个元素。

  • 在指向数组元素的指针上使用解引用操作符 *(dereference operator)自增操作符 ++(increment operator),与在迭代器上的用法类似。
  • 对指针进行解引用操作,可获得该指针所指对象的值。
  • 当指针做自增操作时,则移动指针使其指向数组中的下一个元素。

数组

  • 由类型名、标识符和维数组成的复合数据类型(书第 2.5 节)
  • 类型名规定了存放在数组中的元素的类型,而维数则指定数组中包含的元素个数。数组定义中的类型名可以是内置数据类型或类类型;数组元素的类型可以是任意的复合类型。没有所有元素都是引用的数组。但是不能是引用类型
  • 引用只是已经存在的对象的别名,它是不占用内存空间的,必须先有具体的实体对象,然后才能有引用,而创建数组的时候要给每一个元素分配内存空间,所以数组元素不能是引用。

数组的定义和初始化

  • 数组的维数必须用值大于等于 1 的常量表达式定义(第 2.7 节)
    • 此常量表达式只能包含整型字面值常量、枚举常量(第 2.7 节)或者用常量表达式初始化的整型 const 对象。非 const 变量以及要到运行阶段才知道其值的 const变量都不能用于定义数组的维数
  • 数组的维数必须在一对方括号 [ ] 内指定
  • 不允许数组之间直接复制和赋值(一个数组不能用另外一个数组初始化,也不能将一个数组赋值给另一个数组,这些操作都是非法的)

显式初始化数组元素

定义数组时,可为其元素提供一组用逗号分隔的初值,这些初值用花括号{}括起来,称为初始化列表:

const unsigned array_size = 3;
int ia[array_size] = {0, 1, 2};

显式初始化的数组不需要指定数组的维数值,编译器会根据列出的元素个数来确定数组的长度:

int ia[] = {0, 1, 2}; // an array of dimension 3

如果指定了数组维数,那么初始化列表提供的元素个数不能超过维数值。

  • 如果维数大于列出的元素初值个数,则只初始化前面的数组元素;
  • 剩下的其他元素:若是内置类型则初始化为0,
  • 剩下的其他元素:若是类类型则调用该类的默认构造函数进行初始化.

特殊的字符数组

字符串字面值(第 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 是一个指针变量

注意事项

  1. 避免使用未初始化的指针

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 * 误认为是一种数据类型,二者要分开看

指针可能的取值

有效的指针必然是以下三种状态之一:

  1. 保存一个特定对象的地址;
  2. 指向某个对象后面的另一对象
  3. 或者是0 值。若指针保存0 值(NULL),表明它不指向任何对象。未初始化的指针是无效的,直到给该指针赋值后,才可使用它。

指针初始化和赋值操作的约束

对指针进行初始化或赋值只能使用以下四种类型的值:

  1. 0 值常量表达式(第 2.7 节),例如,在编译时可获得 0 值的整型 const对象或字面值常量 0。
  2. 类型匹配的对象的地址。
  3. 另一对象末的下一地址。
  4. 同类型的另一个有效指针。

除了将在第 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

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

void* 表明该指针与一地址值相关,但不清楚存储在此地址上的对象的类型。
void* 指针只支持几种有限的操作:

  • 与另一个指针进行比较;
  • 向函数传递void* 指针或从函数返回 void* 指针;
  • 给另一个 void* 指针赋值。
  • 不允许使用void* 指针操纵它所指向的对象

指针操作

  1. 指针提供间接操纵其所指对象的功能。与对迭代器进行解引用操作(第 3.4
    节)一样,对指针进行解引用可访问它所指的对象,* 操作符(解引用操作符)
    将获取指针所指的对象.
string s("hello world");
string *sp = &s; // sp holds the address of s
cout <<*sp; // prints hello world
  1. 操纵指针变量中地址所指向的对象:解引用操作符返回指定对象的左值,利用这个功能可修改指针所指对象的值
*sp = "goodbye"; // contents of s now changed
//因为 sp 指向 s,所以给 *sp 赋值也就修改了 s 的值。
  1. 修改指针变量所指向的地址:给指针直接赋值即可修改指针的值——不需要对指针进行解引用。
//修改指针 sp 本身的值,使 sp 指向另外一个新对象:
string s2 = "some value";
sp = &s2; // sp now points to s2

指针和引用的比较

虽然使用引用(reference)和指针都可间接访问另一个值,但它们之间有两个重要区别:

  1. 引用总是指向某个对象:定义引用时没有初始化是错误的。
  2. 赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。引用一经初始化,就始终指向同一个特定对象(这就是为什么引用必须在定义时初始化的原因)。

考虑以下两个程序段。第一个程序段将一个指针赋给另一指针:

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 指针”得到的是指针本身的大小;

  • 指针和引用的自增(++)运算意义不一样;

  • 引用是类型安全的,而指针不是 (引用比指针多了类型检查)

向函数中传递指针和传递引用的区别

  • 如果是传递指针,那么会先复制该指针,在函数内部使用的是复制后的指针,这个指针与原来的指针指向相同的地址,如果在函数内部将复制后的指针指向了另外的新的对象,那么不会影响原有的指针;
  • 指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
  • 引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址(int &a的形式)。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

向函数中传递指针的引用

  • 对于传递指针应用,如果将传递进来的指针指向了新的对象,那么原始的指针也就指向了新的对象,这样就会造成内存泄漏,因为原来指针指向的地方已经不能再 引用了,即使没有将传递进来的指针指向新的对象,而是在函数结束的时候释放了指针,那么在函数外部就不能再使用原有的指针了,因为原来的内存已经被释放了

参考资料

传指针和传指针引用的区别/指针和引用的区别(本质)

你可能感兴趣的:(C++)