C++Primer笔记(第二章)

关键词 : 对象 , 变量 , 引用 , 指针 , const , auto , decltype

第二章 :

有关对象 :

对象是指一块能存储数据并具有某种类型的内存空间 , 当对象创建时获得了一个特定的值 , 这个行为被称作初始化 . 初始化与赋值的区别在于赋值是把对象的当前值擦除 , 而以一个新值来替代.(“擦除”和”替代”)

有关变量 :

声明 : 规定了变量的类型和名字

定义 : 任何包含了显式初始化的声明即成为定义(申请储存空间并赋值)

如果想声明一个变量而非定义 , 就在变量名前添加关键字extern, 而且不要显式的初始化变量 . 在函数体内部 , 如果试图初始化一个extern关键字标记的变量将引发错误.

变量能且只能被定义一次 , 但是可以被多次声明.(怎么理解?)

变量的定义必须出现在且只能出现在一个文件中 , 而其他用到该变量的文件必须对其进行声明 , 却绝对不能重复定义.

有关作用域 :

::称为作用域操作符 , 当左侧为空的时候 , 向全局作用域请求获得操作符右侧名字的变量.

有关引用和指针 :

引用和指针应该算是第二章里比较重点 , 同时也比较绕的内容 , 特将内容梳理如下以备查阅与理解:

引用 : 引用是为已经存在的对象起的另外一个名字.

  • 引用必须被初始化 : 在定义引用的时候 , 程序就把引用和它的初始值绑定在了一起 , 因为无法重新绑定到其他对象 , 所以引用必须被初始化 .
  • 因为引用本身不是一个对象(可寻址的空间) , 所以无法定义引用的引用.
  • 引用只能绑定在对象上 , 而不能绑定在字面值或者某个表达式的计算结果上.(原因 : 首先字面值本身不是一个对象 , 所以不能绑定 , 表达式的计算结果应该是一个临时变量 , 也不是一个对象所以无法绑定 ? 怎么理解 ?)

指针 : 指针本身就是一个对象 , 允许对指针进行赋值以及拷贝 , 在指针的生命周期里可以先后指向不同的对象 . 指针无需在定义时赋初值 , 如果定义时未初始化则有一个不确定的值.

同样(与引用类似) , 一个指向常量的指针可以指向一个非常量对象.

引用不是对象 , 没有实际地址 , 所以无法定义指向引用的指针.

指针的值应属于以下四种状态之一 :

  1. 指向一个对象
  2. 指向紧邻对象所占空间的下一个位置
  3. 空指针 , 没有指向任何对象
  4. 无效指针 , 除了上述情况的其他值

指针伴随着一对操作符 : 取地址符&和解引用符* , 前者获得对应对象的地址 , 后者获得地址所指的对象(解引用操作仅适用于那些指向了有效地址的指针).

尽量初始化所有的指针 , 并且在可能的情况下 , 尽量等定义了对象再定义指向它的指针 , 如果不知道应该指向何处 , 可以把它初始化为nullptr或者0 .

指针可以使用==进行比较 , 当指向的地址值相同时为true.

void 是一种特殊的指针类型 , 可以用于存放任意对象的地址 . 可以用来拿它和别的指针比较 , 作为函数的输入和输出 , 或者赋给另外一个void指针 .

关于复合类型的声明 :

  • 最简单的办法就是从右向左阅读r的定义,离变量名最近的符号对变量的类型有最直接的影响 .

  • 关于const限定符 :

    const对象必须初始化.

    只能在const类型的对象上执行不改变其内容的操作 , 默认情况下 , const变量仅在文件中有效 , 当多个文件中出现了同名的const变量时 , 其实等同于在不同文件中定义了独立的变量.

    如果想在多个文件之间共享const对象 , 必须在变量的定义前添加extern关键字.

    可以把引用绑定到const对象上 , 称之为对常量的引用 , 不能被用作修改它所绑定的对象.

    引用的使用场景(怎么理解?) :

    1. 别名–>减少拷贝开销
    2. 参数传递–>减少拷贝开销 , 避免空指针
    3. 函数返回值–>
1
2
3
4
5
6
7
8
9
double dval = 3.14;
const int &ri = dval;

/* 编译器做法 */
const int temp = dval; // 让double类型生成一个临时的int变量
const int &ri = temp; // 让ri绑定这个临时量

/* 因为不可以通过被const修饰符限定的常量引用修改被引用的对象,所以该引用可以绑定在临时量上,不会对临时量产生修改的操作; 如果是非常量引用,则可能通过该引用修改对应的临时量,但是代码的目的是通过ri来修改试图绑定的dval,却绑定到了临时量上,与预期不符,所以非法 */
int &ri = dval; // 非法

img

常量引用仅仅对引用可参与的操作进行了限制 , 即不能通过该引用改变其引用的对象 , 对于对象本身是否是常量未做限定.

指针和const

  • 指向常量的指针(pointer to const)不能用于改变其所指对象的值 , 存放常量对象的地址 , 只能使用指向常量的指针.

    1
    2
    3
    4
    const double pi = 3.14;
    double *ptr = π // 非法
    const double *cptr = π // 合法
    *cptr = 43; // 非法, 不能给*cptr赋值.

同样指向常量的指针也对指向的对象本身是否是常量未做限定 , 仅仅是不能通过该指针改变所指对象的值.

  • 常量指针(const pointer) , 指针本身是常量 , 根据const的规则 , 必须初始化 , 初始化完成后其值不可改变 , 不可变的是指针本身而不是指向的值 , 是否可以通过该指针改变其指向的值取决于它指向的值是否是常量.

    “是否可以通过该指针改变其指向的值, 取决于它指向的值是否是常量” , 这句话并不意味着常量指针可以指向常量对象, 详见例子:

    1
    2
    3
    4
    5
    const int a = 1;
    int *const p = &a; // 错误, 常量指针指向了常量对象
    const int *p1 = &a; // 正确, 指向常量的指针指向了常量对象
    int *p2 = &a; // 错误, 普通指针指向了常量对象
    const int *const p3 = &a; // 正确, 指向常量的常量指针指向了常量对象, 此处对常量对象的引用主要取决于指向常量的指针, 对于指针本身是否是常量并不做要求

顶层const与底层const

顶层const(top-level const) : 表示指针本身是个常量

底层const(low-level const) : 表示指针所指的对象是个常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int i = 0;
int *const p1 = &i; // 指向int的常量指针,top-level
const int ci = 42; // 常量int, top-level
const int *p2 = &ci; // 指向常量int的指针, low-level
const int *const p3 = p2;// 指向常量int的常量指针,第一个为low-level,第二个为top-level
const int &r = ci; // 用于声明引用的const都是底层const(怎么理解? 不能通过引用改变绑定的变量, 所以是底层const? 换句话说, 常量引用认为自己绑定的变量是个常量?)

/* 当执行对象的拷贝操作时, 拷入和拷出的对象必须有相同的底层const资格, 或者两个对象的数据类型可以相互转换, 非常量可以转换成常量, 反之不行*/
int *p = p3; // 错误: p3包含了底层const, 如果正确则可能通过p改变p3指向的值, 与const冲突
p2 = p3; // 正确: p2和p3都是底层const
p2 = &i; // 正确: int *可以转换成const int *
int &r = ci; // 错误: int&不能绑定到const int上
const int &r2 = i; // 正确: const int& 可以绑定到int上

/* 可以通过"试图改变某对象"的思路来判断是否正确, 不过这些内容应该编译器都会提示吧...... */

constexpr和常量表达式 :

值不会改变并且在编译过程中就能得到计算结果的表达式 .

1
2
3
4
5
int staff_size = 27;		// 不是常量表达式, staff_size可以改变
const int sz = get_size(); // 不是常量表达式, sz的具体值直到运行时才能获得

const int *p = nullptr; // 指向整形常量的指针
constexpr int *q = nullptr; // 指向整数的常量指针

将会提到 , 函数体内定义的变量一般来说并非存放在固定地址中 , 因此constexpr指针不能指向这样的变量, 定义于所有函数体之外的对象地址固定不变, 能用来初始化constexpr指针 , 允许函数定义一类有效范围超出函数本身的变量, 这类变量和定义在函数体之外的变量一样有固定地址, 因此, constexpr引用能绑定到这样的变量上, constexpr指针也能指向这样的变量. (后面提到再看吧……)

关于类型别名 :

可以使用typedef和using两种关键字进行定义.

1
2
3
4
5
6
7
8
typedef char* pstring;
const pstring cstr = 0; // 指向char的常量指针
const pstring *ps; // 指向一个指向char的常量指针的指针

const char* cstr = 0; // 指向const char的指针
const char **ptr; // 指向一个const char指针的指针
/* const是对给定类型的修饰, 所以const pstring为指向char的常量指针, 而非指向常量char的指针*/
/* 不能通过替换类型别名来理解 */

关于auto类型说明符 :

auto定义的变量必须有初始值 .

当引用被用作初始值时 , 真正参与初始化的其实是引用对象的值 , 编译器以引用对象的类型作为auto的类型.

auto一般会忽略掉顶层const , 同时底层const则会保留下来 :

1
2
3
4
5
6
7
int i = 0, &r = i;
auto a = r;
const int ci = i, &cr = ci;
auto b = ci; // b推断为一个int(ci作为top-level被忽略)
auto c = cr; // c推断为一个int(cr是ci的引用,ci本身是一个top-level) ??? cr不是一个底层const吗(理解: cr是ci的引用, c是ci的copy, 目的不是通过c去改变ci, 所以c推断为一个int)

const auto f = ci; // f为const int, 需要显式指定const来推断出top-level const

符号&和*只从属于某个声明符 , 而非基本数据类型的一部分 , 因此初始值必须是同一种类型.

decltype类型指示符:

类似于lua中的type()用来推断类型, 它的作用是选择并返回操作数的数据类型 , 编译器分析表达式并得到它的类型, 却不实际计算表达式的值.

1
decltype(f()) sum = x;        // sun类型为函数f的返回类型

如果decltype使用的表达式是一个变量, 则decltype返回该变量的类型 (包括top-level const和引用在内) ; 如果decltype使用的表达式不是一个变量 , 则decltype返回表达式结果对应的类型.

decltype((variable))的结果永远是引用 , 而decltype(variable)结果只有当variable本身是一个引用时才是引用.

1
decltype((i)) d;			// d是int&, 必须初始化

有关头文件 :

头文件通常只包含那些只能被定义一次的实体, 如类 , const和constexpr变量.

1
2
3
4
#define		// 把一个名字设定成预处理变量
#ifdef // 当且仅当变量已定义时为真
#ifndef // 当且仅当变量未定义时为真
#endif // 结束符