======变量和基本类型====== C++ Primer 笔记 第二章\\ ---- 在 C++ 中,基础类型(//Primitive types//)包括算数类型(//Arithmetic types//)和一种特殊的类型 ''void''。 Void 类型主要适用与**无返回值的函数**。 ====算术类型==== 在C++中, 整型(//Integral//)和浮点型(// Floating-point//) 统称**算术类型**。而整型又分为3种类型:''integers'' , ''boolean'' 和 ''char''. - //bool//, 存放 ''true'' or ''False'' - //char//, 存放字符,C++ 中有比标准 ''char'' 大一倍的 ''wchar_t'',有适用于 Unicode 的 ''char16_t'' / ''char32_t''。 - //int//, 存放整数,有 ''short'' / ''int'' / long / ''long long'' 适应不同大小的数。 计算机使用二进制序列(比如01010101)来处理数据。数列中的每一位称为 **bit**。大部分情况下,计算机会将二进制序列按块划分,块的大小往往是 2 的 n 次方。这些块中,最小的被称为 **Byte**(Byte 需要足够大来装载计算机中的基础字符集)。Byte 拥有自身唯一的地址。 \\ 计算机一次运算能够处理的 Byte 的数量,称为 **Word**,这也往往是存储器的单位大小。现代计算机的 Byte 大小一般为 8 bits, word 大小为 32 或 64 bits. ===unsigned 类型=== 对于所有 //Integral// 类型的变量来说,除开 ''bool'' 类型,''int'' 和 ''char'' 都可以带有符号或者不带有符号。**跟** ''int'' **不同**, ''char'' **有三种类型:** ''char'', ''signed char'', ''unsigned char''. \\ \\ unsigned 类型的所有位数都用于存储值,因此一般来说范围是从 0 开始。signed 类型由于需要多出一位记录正负,因此能表达的数值范围会以 0 为中心点。 ===关于类型使用的注意事项=== - ''unsigned'' 类型请使用非负值赋值。 - 对整型操作中,''int'' 会比 ''short'' 类型使用起来更方便。 - 不要将 ''bool'' 和 ''char'' 用于算术表达式中。 带符号的 ''char'' 类型会在不同的系统里表达出不同的正负。如果非要使用请指定正负。 - 浮点数运算请使用 ''double''。''double'' 的精度更高,在现代计算机上有时候速度反而比 ''float'' 快。 ===类型转换=== ==Bool 与其他类型的转换== * nobool -> bool : ''0'' = ''False'', 其他值 = ''True'' * bool -> nobool(算数类型) : ''False'' = 0, ''True'' = 1 ==Float point 与 整型的转换== * ''float-point'' -> ''intergral types'': value truncated,保留小数点之前的部分。 * ''intergral types'' -> ''float-point'': 小数部分记 0,如果整数占用 bits 大于浮点数,精度可能会丢失。 ==out of range value== * 超过范围的值 -> ''unsigned'' 类型: **求模运算**。比如:''unsigned char c = -1; -1+256 = 255;'' 所以 ''c'' 值应该等于 ''255''. * 超过范围的值 -> ''signed'' 类型: //undefined//. 在很多环境中,编译器不会(也不能)检查**未定义行为**(//Undefined behavior//)。同时,是否能检查出未定义行为也跟具体的编译器有关。因此,我们应该尽量避免某些基于实现环境依赖的行为(比如当前编译器认为 int 为 16 位,在实现中就按 16 位的大小来处理 int)。这样的程序是不可移植的,而且编译器很可能找不出这样的错误。 ==unsigned+singed 运算导致的类型转换== 如果进行 unsigned+singed 运算,**结果将会被转换为 unsigned 类型**。也就是说,如果结果为负数,会进行取模运算。 如无必要,请不要把 **unsigned** 和 **signed** 类型混淆运算。 ===Literals=== **字面值常量**(//Literals//) 实际上指值不变的变量,也就是常量。字面值常量的类型和值决定了它自身的数据类型。 ==整型 / 浮点型的字面值== 整型和浮点型的字面值可以写成十进制(decimal)、八进制(octal)或者十六进制(hexdecimal)的形式,比如: 20 decimal //signed by default 024 octal //start with 0 0x14 hexadeicmal //start with 0x 当本类型常量为八进制或者十进制的时候,其数据类型为最小的可以容纳下该数据的类型(比如 int 能容纳下,就是 int,否则就尝试 long,以此类推)。\\ \\ 技术上来说,十进制下的常量不会为负值。也就是说,符号不包含在字面值常量中,而是一种对字面值常量取负的运算。\\ \\ 浮点型的字面值为**小数**,可以由科学计数法的小数表示。 ==字符 / 字符串字面值== * 字符 / 字符串的字面值由单引号 / 双引号之间的内容表示。 * 字符串比看上去的字面值长度要大 1,因为编译器会在字符串末尾加 ''\0'' 作为结束的标记。 * 如果字符串之间仅由空格、缩进、换行符分割,那么实际上他们是一个整体。 ==转义序列== Escape Sequence 是 char / string type literal,因此输出的时候需要加上 quote. Ref:[[https://en.cppreference.com/w/cpp/language/escape|ecsape sequences]]\\ \\ **转义序列**(//ecsape sequences//)指需要打印(不可打印字符或特殊字符)时需要用到的命令,比如 ''\n'' 等等。所有的这些命令必须要使用 ''\'' 开头才能生效。 {{ cs:programming:cpp:cpp_primer:escape_seq.jpg?400 |}} ==Numeric escape sequences== 也被称为 generalized escape sequence。该序列通过两种组合方式来表现不同的不可打印字符(总之就是用数字来表示对应的字符): * 以 ''\x'' 起头,后面接 1 位或者更多位的十六进制数,比如 ''\x4d'' * 以 ''\'' 起头,后面接 1位或者更多位数的八进制数,比如 ''\115'' 上面的数对应了字符的字符值([[https://www.asciitable.com/|ascii 值,简单的对照表]]) ====变量==== 变量被存储于计算机里,可以被计算机做修改操作。C++ 中变量的一般包括变量( //Variables// )和对象( //Objects// ) ===变量的定义与声明=== 在C++中,变量的**定义**( //Definition// )与**声明**(//Declaration// )是分开的。C++ 在编译的过程中是支持**分别编译**( //Separate Compliation//) 的。为了支持这种行为,**C++ 严格区分了定义和声明**。声明指定了变量的名字,而定义建立了与之名字对应的实体(分配存储空间,赋予初始值)。 所以,任何带有初始值的声明,都是定义。\\ \\ 因此,我们通过在一个文件中定义一个对象(变量),然后在其他文件中需要使用的时候声明便可以使用了。正常的使用流程是,首先做出定义(存到指定的 header),然后在到需要使用的地方声明一遍即可。 \\ ==keyword extern== 如果希望只声明而不定义变量,可以使用 ''extern'' 关键字。有两点要注意: * 如果在含有 ''extern'' 的声明中进行了**赋值初始化**,那么表达式就会**从声明转化为定义**,''extern'' 的功能将会被**覆盖**。 * ''extern'' **不能用于函数内部**。 extern int i; //declaration but not definition extern int i = 10; //definition, extern has been overridden. C++ 是静态语言,因此在编译的时候编译器必须知道变量的类型。编译过程中编译器会检查程序中的运算是否支持当前变量,如果不支持则会报错。 ===整型变量的定义和初始化=== 在C++中,初始化和赋值是两个完全不同的概念。**初始化发生在对象创建的时候,而赋值是替换已存在对象的值。** ==初始化的形式== C++提供了4种形式的整形变量初始化: int a = 0; // expression char c = {'x'}; // { initializer-list } int b{0}; // { initializer-list } char d('y'); // ( expression-list ) ==List 初始化== 其中带 //curly braces// 的初始化形式是新标准, 称之为 //List Initialization//。 C++ Primer 的作者推荐使用这种形式。原因是 List 初始化自带类型校验:List 初始化不允许**类型转换** (//Type Conversion//) 带来的 //Narrowing Conversion//。比如: double x = 3.1415926; int a{x}; // error: narrowing conversion int b(x), c = x; //ok: b and c are truncated ==默认初始化== 如果在初始化的时候没有制定初始化的值,那么编译器就会对变量做**默认初始化**( //Default Initialization// )。默认初始化的值跟以下两点有关系: - 对象初始化的位置:算术类型定义在函数**外部**的,默认值为 ''0'',定义在函数**内部**的,会导致 //undefined//。 - 对象的类型: 上述的规则是针对于 //Build-inType// 和没有定义默认初始化的自定义类型来说的。如果自定义类型(比如类)中有默认构造函数进行初始化,那么**该类型的默认初始化值就是该类型定义的初始值**。 ==Identifiers / 如何命名== 总的来说,Identifiers 指广义范围内变量 / 对象的名字。Identifiers 有几个特性: * 必须以**字母**(//letters//)或者**下划线**(//underscore//)开头。 * 区分大小写。 * 不能与系统保留的关键字重名。 一些约定俗成的规矩: * Identifiers 需要表意 * variable Identifiers 一般情况下小写 * class Identifiers 一般开头大写 * 较长的 Identifiers 使用下划线或者骆驼写法区分,比如 ''student_loan'' 或者 ''studentLoan''。 === Scope of a name=== 如果希望同一个 name 对应不同的实体,可以使用 //Scope// 实现。通过在指定的 scope 内声明变量,就可以在指定的作用域内绑定name 与实体。作用域为的 scope 的范围,其通过 //curly brace//来指定。有几个例子: * ''main{}'',自身就是一个 scope。这种定义在所有 scope 外部的 scope,称之为 //Global Scope//。只要程序正在运行,该类 scope 就会一直存在。 * 在 scope 中定义的变量,其作用域范围从其被看见(声明)的位置开始直到该 scope 结束。这种变量具有 **block scope**。 变量 / 对象的定义最好是放置在其对应的 scope 附近。这样可以提高程序的可读性,也更容易给与该变量/对象一个有用的初始值。 ==Nested Scope== scope 是可以嵌套的。外部的被称为 outter scope,内部的则被称为 inner scope。通常情况下: * outter scope 定义的变量可以被 Inner scope 使用 * inner scope 可以定义与 outter scope 名字相同的变量,并**重新定义**,也就是**就近使用** 如果在 inner scope 已经做了与外部变量重名的定义,但又要使用外部重名的变量,可以使用 //Scope operator// ''::'' 调用外部变量: #include int reused = 42; int main(int argc, char const *argv[]) { std::cout << reused << std::endl; //print the outter reused, the 42 int reused = 0; std::cout << reused << std::endl; //print the inner reused, the 0, outter definition overwrited std::cout << ::reused << std::endl; //print the outter reused, the 42, explictly accesss the outter variable return 0; } ====复合类型==== **复合类型**( //Compound Type// )是指基于某种指定的类型而定义下来的类型。C++ 中 比较重要的复合类型有:**指针**(//Pointer// )、**引用**(//Reference//)。 ===引用=== 引用定义了一个**对象的别名**(//Alternative Name//)。我们可以把引用的定义看做是一种绑定(//bind//)。因此在定义引用的时候,必须对引用进行初始化。比如: int ival = 1024; //object will be bind. int &refVal =ival; //a reference will bind to ival int &refVal2; // error: no binding object, reference must be initialized. ==引用的定义== 我们用符号 ''&'' 来定义引用。引用的定义方法如下: int i = 1024, i2 = 2048; //two int objects that will be used for bind int &r = i; &r2 = i2; // r是i的引用,r2是i2的引用。 ==引用的几个注意点== - 引用**必须指向一个对象**(必须用对象来初始化 / 必须绑定对象),**literal 不能直接引用。** - 引用自身不是对象,是别名(所以不存在引用的引用)。 - 引用的类型必须和被引用的对象类型一致。 Reference to const 是可以直接引用 literal 的。 [[https://stackoverflow.com/questions/2088259/literal-initialization-for-const-references|Literal initialization for const references]] ===指针=== 指针(//Pointer//)与引用类似,也是通过间接方式访问对象(通过地址)。但是指针跟引用不同在于,**指针本身自己是个对象**。所以,指针具有对象(变量)的一切性质:可访问,可赋值,等等。 相较于引用,指针拥有以下特性: * 可以被赋值和复制 * 可以改变指向的内存位置 * 定义的时候不需要被初始化(不用绑定已存在对象) 类似 build-in type,函数内的**指针默认初始化**也会导致** //Undefined//**。 Ref:[[https://stackoverflow.com/questions/57483/what-are-the-differences-between-a-pointer-variable-and-a-reference-variable-in/57492#57492|What are the differences between a pointer variable and a reference variable in C++?]] ==指针的定义== 指针变量的定义使用 ''*'' 符号进行定义。同时,指针是用于存储内存地址的对象,所以指针的初始化值**必须是一个地址**。我们用 ''&'' (在定义中是引用,在运算中是取地址操作符)来得到对象的地址,那么指针的一般初始化形式就应该是: int i = 1; int *p = &i; //store the address of i in pointer p 同时定义几个指针的时候,每个指针前面必须加上 ''*'' 号,例如: int *p1, *p2; 需要注意的是: * 引用自身并没有地址,因此不能创建一个指向引用的指针. * 指针在 initialization / assignment 的时候,**类型必须与所处指向地址的数据类型相同**(指向不同的类型,或者指向指向该类型的指针,都是不行的。) double dval; double *pd = &dval; // ok: initializer is the address of a double, address to address double *pd2 = pd; // ok: initializer is a pointer to double, address to address int *pi = pd; // error: types of pi and pd differ pi = &dval; // error: a pointer pointed to double can't assign to an int pointer ==指针的形式== * 第一类,指向某个对象 * 第二类,指向某个位置,该位置正好在某个对象占用空间的下个地址格 * 第三类,不指向任何对象,也就是空指针 * 除以上三种,都是无效指针(对此类型指针操作都将导致 undefined 行为) 需要注意的是,**访问**所有没有**指向明确对象的指针**都属于 undefined 行为。 ==使用指针访问== ''*'',在运算中被称为解引用运算符(//Dereference Operator//)。我们使用该运算符来获取指针指向的对象中的内容: int ival = 42; int *p = &ival; // 这里的 * 是定义,是定义指针的标识,不是解引用。这里的 & 是运算,是取对象地址的运算,不是定义引用的标识。 cout << *p; // 这里的 * 是运算,是解引用,是将指针指向的对象中的内容取出。 再次强调,安全访问指针指向的内容的前提是**该指针必须指向一个存在的对象**(第一类指针)。换句话说,尽量使用初始化后的指针。访问指向未初始化内容的指针会带来一系列的后果,包含但不限于: * run-time crash,而且非常难以 debug * 指针可能指向正在被程序其他部分访问(使用)的内存,导致非法的内存空间申请带来的冲突。 如果没有可以指向的对象,尽量将指针初始化为空指针。 ==关于 '*' 和 '&' 的用法 == * 定义下的用法(//Type modifier//): * ''*'' 用于定义指针,''int *p;'' 这里的 ''*'' 代表 //p// 是一个指针。 * ''&'' 用于定义引用:''int &r = i;''。这里的 ''&'' 代表 //r// 是绑定到 i 的引用。 * 运算下的用法 * 访问指针/引用指向的元素的时候(Derefenece): ''*p = i;'' 。这里的 ''*'' 是**解引用运算符**。 * 取地址的时候:''p = &i;''。 这里 ''&'' 代表取 //i// 的地址。 总的来说,就是在声明的时候,''&'' 和 ''*'' 都是用于定义复合类型的。在表达式中,这两个符号都是运算符(''*'' 还可以用作乘法运算),一个是解引用(访问地址中的内容) 一个是 取地址(访问指针中的地址值)。 ==空指针== 如果定义指针的时候不明确指向对象,我们可以用到 ''null'' 指针。''null'' 指针不指向任何地方,只是用来初始化。在C++11中,作者推荐用 ''nullptr'' 来取代 ''NULL'' (C 语言中 ''cstdlib'')对空指针进行初始化。\\ \\ 为什么在C++中使用 ''nullptr'' 取代 ''NULL'' 可以参考这篇文章: [[http://example.com| C++11中的 nullptr 详解]] 某些情况下给指针赋值 0 也等同于创建空指针的效果。但需要注意的是,这里的 0 只能是 literal,将值为 0 的 int 类型变量赋值给指针是非法的。 ==指针的赋值== 对指针的赋值和对引用的赋值是不同的。引用一旦绑定,就不可能再进行引用对象的更改,任何对引用的操作都是对其初始对象的操作。指针则不同,指针是可以通过赋值来改变指向位置的: int i =42; int *pi = 0; //initialize pointer pi as a null pointer. int *pi2 = &i; //initialize pi2 as a pointer points to the addresss of i int *pi3; //bad pratice, pi3 would be uninitialized if it is not a global pointer pi3 = pi2; // pi3 and pi2 point to the same address pi2 = 0; // pi2 points is null pointer now. 一个通用的规则是,**赋值改变的是**赋值运算符左边算子**的内容**。这个算子是指的一个**整体**。比如: pi = &ival; //改变的是 pi 的内容 那么如果是如下形式: *pi = 0; //改变的是 pi 指向的内容 由于这里是赋值运算,那么左边的 ''*'' 被视作解引用运算符,左边整体可以被视作被 ''pi'' 指向的对象。则上面的语句实际上将 ''pi'' 指向的对象中的内容改写为了 0,但该对象在内存中的地址,也就是 ''pi'' 中存储的地址,是没有发生变化的。 ==指针的其他运算== * 指针在某些情况下可以用做条件判断。当指针为**空指针**的时候,条件为假,反之为真。 * 指针之间可以进行比较(通过比较运算符 ''=='' 等等)。两者相等的条件是两个指针指向的地址相同(或者是指向相同对象的二类指针,或者是都指向空指针) 还有一类特殊类型的指针 ''*vold''。这种类型的指针与普通指针不同的地方在于它可以指向**任意类型**的对象(有点 O型血的那么个意思)。以下是几种该指针的应用: * 与其他指针比较 * 传递给函数或者作为函数的返回值 * 赋值给另外一个 void 类型的指针 这种指针的局限性在于,**不能通过 void 指针来对其指向的内容进行操作**,因为对指向对象操作的前提是必须知晓该对象的类型。 ===复合类型的声明=== ==定义多个变量的情况== 在定义(''*'' 或者 ''&'' 作为 type modifier 使用)的时候,type modifier 只对最近的一个变量起作用,因此定义多个变量需要在每个变量前都加上 type modifier: int* p; //bad practice, misleading int *p0; //good practice to define a pointer with int type int* p1, p2; //p1 is a pointer , p2 is an int variable int *p3, *p4; //good way to define multiple pointers in 1 statement. 在定义复合类型的时候,Type Modifier 是每个变量不可缺少的一部分。本书推荐使用 type modifier 靠近 变量名的写法。 ==指向指针的指针== 指针本身是对象,也会拥有自身的地址。因此定义指向指针的指针也是可以的: int i = 1024; int *pi = &i; int **ppi = π //&pi is the address of the pointer, not the address which pointer points \\ \\ {{ :cs:programming:cpp:cpp_primer:ppi.svg?250 |}} \\ \\ 当然,如果需要通过 ''ppi'' 来访问 ''i'' 的内容,我们需要解引用两次: std::cout << **ppi << std::endl; ==指针的引用== 引用不是对象,因此不存在指向引用的指针。但反过来则可以: int i = 0; int *p; int *&r = p; //reference to pointer r = &i; //r is an altertive name to p, so assign the address of i to r makes p points to i. *r = 0; // dereference r means dereference p, which yields i. then changes i to 0 复合类型的混用向来比较让人困惑。书上讲了一个阅读的规则,那就是从右往左读定义。也就是说,**离变量名右侧最近的type modifer,决定了该定义的复合类型**(比如下面在 *&r 中的 & )。\\ \\ 比如这个例子: int *&r = i; 来分析一下这个步骤: - 首先找到 ''r'',确定变量名 - 往右看看到 ''&'',确定定义的是名称为 ''r'' 的引用 - 继续往左看则是声明的引用 refer 的指针类型:''int*'',这里实际上写成 ''int* &r = i'' 更好理解。 因此这就是一个与整型指针绑定的引用的声明过程。 ====Const 修饰符==== ''const'' 修饰符(//Qualifier//)是用来定义我们不希望被改变的变量。因为这个变量不能再建立之后改变,所以我们只能在初始化的时候给其赋值,也就是说:''const'' 修饰的变量必须被初始化。 const int i = 10; //ok const int j; //error, const variable must be initialized. i = 20; //error, attempt to write to const object 我们也可以用表达式来作为 ''const'' 初始化的值,比如: const int k = getsize();//initialized at runtime const 只对当前初始化的变量起作用,旨在限制对当前变量的修改操作。只要不涉及到更改变量的操作,const 对象与普通对象的操作是一致的。 ===Const 作用范围是文件以内=== 由于编译的时候,''const'' 变量会被编译器自动替换为其值,编译器会在编译期寻找 ''const'' 变量对应的初始值。因此 const 修饰的变量是必须是编译器可见,即拥有完整定义的变量。由于 C++ 支持分离编译,如果该变量在多个文件中均存在,那么需要在每个文件中都有定义。但问题在于,C++ 遵循唯一定义规则,因此需要通过两种方法来解决: - 针对每个文件单独创建一个 const 变量。 - 通过 ''extern'' 关键字共享唯一的 const 变量。 第二种情况下,为了使用外部的 const 变量,我们需要定义以及声明两个步骤: * 在指定的源文件(.cc / .cpp)中定义 const 变量。这里的 const 变量可以接受对象,甚至函数: /*file.cc, defines and initializes a const, that is accessible to other files*/ extern const int bufSize = fcn(); //initializer can be a function * 之后通过头文件对该变量进行声明,来达到共享的目的: /*file.h, declar the bufSize. Include the header to share variable bufSize*/ extern const int bufSize; 一个简单的多文件共享全局变量的例子如下: * ''extern_const_test_def.cpp'' 文件用于**定义**需要共享的全局变量 * ''extern_const_test_def.h'' 用于**声明**需要共享的全局变量 * ''extern_const_test.cpp'' 用于**使用**共享的全局变量 /*extern_const_test_def.cpp*/ extern const int bufSize = 512; /*extern_const_test_def.h*/ extern const int bufSize; /*extern_const_test.cpp*/ #include #include "extern_const_test_def.h" int main(int argc, char const *argv[]) { std::cout << "The size of global buffer is: "<< bufSize << std::endl; return 0; } 编译以及链接的过程: #分别对 cpp 文件进行编译 #如果没有 main 函数,g++ 需要使用 -c 来编译 c++源文件 g++ -c extern_const_test.cpp g++ -c extern_const_test_def.cpp #对得到的 .o文件进行链接 g++ -o a.out extern_const_test.o extern_const_test_def.o #访问 ./a.out ===Const、指针和引用=== ==Reference to const== 对 const 的引用指“reference that refers to a const type”,基于const 修饰类型的引用。这类引用确保了**不能通过该引用**修改绑定对象的内容。 const int ci = 1024; const int &r1 = ci; // defines a refernece refers to a const variable int &r2 = ci; // error. a plain reference can't refers to a const object 绝大多数情况下,引用的类型必须与被引用的对象一致;但有一种例外:如果被绑定的对象的类型,能够通过类型转换变得与引用的类型一致,那么这样的对象也是可以用于引用的初始化的: int i = 42; //variable i const int &r1 = i; // r1 is the reference refers to a const varible, initialized by a non-const int object const int &r2 = 42; // ok. initializor can be convert to const int const int &r3 = r1*2; //ok. initializor can be convert to const int int &r4 = r1 * 2; //error, const int initializor can't be convert to non-const int. 这种情况下,引用的绑定实际上分为两步: - 创建一个临时的 const 对象,使用目标对象对其进行初始化; - 将引用与该临时的对象绑定 类型转换将发生在第一步,也就是说临时对象的类型会与引用一致。那么实际上,我们定义的引用绑定的是一个不可更改的,经过类型转换的临时对象。该对象显然不能被 refer to non-const 的引用绑定。 \\ \\ 只要被绑定的对象可以转化为 const 类型,那么无论被绑定的对象是不是 const, 都不会影响 reference to const 的初始化(注:literal 也可以通过上述步骤转化为临时对象!)。下面是一个实例: int i = 42; int &r1 = i; //ok, non-const bind to non-const const int &r2 = i; // ok const bind to non-const //*now, r1 and r2 refer to the same int variable i*// r1 = 0; //ok, we can modify i through non-const reference r1 r2 = 0; //error, we can't modify i through const-const r2 /*and finally i is changed to 0*/ 可以将 reference 理解为门,reference to const 是一扇关闭的门,告诉你不能通过这扇门去改变屋里的环境。至于有没有其他门,这扇门保证不了。 Ref:[[http://stackoverflow.com/questions/10801894/const-reference-can-be-assigned-an-int|const reference can be assigned an int?]] ==Const 和指针== 指针是对象。所以 ''const'' 和指针连用就分成了以下的两种情况: * pointer to const:与 reference to const 类似,指不能通过该指针去修改被指向的内容(指针绑定的内容) * const pointer:指该指针指向的地址无法被改变(指针本身) ==两种情况的初始化== 这两种组合的初始化也比较容易混淆: int i = 10; const int *p = &i; //define a pointer to const int *const q = &i; //define a const pointer that point to i 这里我们的从右向左看的方法依然适用(C语言中还有 ''int const *p'', 这个写法跟 ''const int *p'' 等同)。\\ \\ 还有一个用法: const int *const ptr = &i; // ptr is a const pointer,its value can't be changed, and the value pointed by it cannot be changed through itsself(the pointer). 同样先前说到的初始化的时候类型要匹配规则同样适用于这里。比如: const double x = 3.1415; double *xptr = &x;// error, type not match. ==Top_level 和 Low_level== 根据 pointer 与 const 的两种组合方式,我们可以定义两种等级: * 自身为 const 的等级为 top level,比如 const pointer * 无法通过自身修改对应内容的等级为 low level, 比如 pointer to const 示例代码如下: int i = 10; // const pointer, its-self cannot be changed -> this const produces a forbidden operation of manipulating the object -> top_level int *const p = &i; // pointer to const, the pointer its-self can be changed, but the value of i can't be changed by using the pointer(expression)-> this const produces a forbidden operation of manipulating the i(what the expression did once)-> low_level const int *q = &i; 还有一个例子: const int *const ptr = &i; //right-most const is top_level, left_most is not 右边的 const 定义了 指针,因此是 top level, 左边的 const 定义了该指针指向的是一个不能改变的 int 值,因此属于 Low level。\\ \\ 之所以提出这两个 level, 是因为绝大多数的,带 const 初始化/赋值的合法性,都可以使用该概念来判断: \\ \\ 首先,拷贝 const 对象的时候,top-level 可以无视,因为拷贝并不影响 const 对象的内容: int i = 0; const int ci = 42; i = ci; //top-level ci is ignored. 其次,low-level 的 const 在任何时候都不能忽略。如果想要成功拷贝,那么等号两边都必须是 Low-level 的 const,或者有可以转换为 const 的对象;因此,non-const 可以转化为 low-level 的 const,但反过来不行(也就是不能拷贝 const 到 non - const): /*接上面*/ int &r = ci; //error, ci is on low-level, r must at the same level. 下面是特殊情况,指向 const 且自身是 const pointer: /*接上面*/ const int *p2 = &ci; const int *const p3 = p2; 该指针左边的 const 为 low-level, 右边的 const 为 top-level。因此: * p3 可以初始化、赋值 p2, 拷贝的时候 top-level 自动忽略了。 * p2 可以初始化 p3。两边的 low-level 匹配。 个人感觉拷贝的过程中,只要是 top-level 的 const 就可以不用管。因为 top-level 的 const 只强调对象本身的不可更改性;也就是拷贝过程中只需要考虑两边 low-level 的匹配就行。通俗点来说,如果 low-level const 是不能打开,但可以装到任意的墙上的门。那么 top-level const 就是让门自身无法被移动和摧毁的力场。这样一来,我们就有两个任务: * low-level const 对象初始化 top | low-level 混搭的对象:这种情况就像是工头指定了位置让你装门,门外已经有力场保证了你的门无法被移动和摧毁。你只要保证你的门打不开就对了。 * top | low-level 混搭的对象初始化 / 赋值 low-level const 对象:工头让你找一扇不能开的门随便找个地方装了。同样你只需要保证你的门打不开,至于那个门之前在不在力场里工头根本不 care。 const 语句的合法性,归根结底诠释了一个权限的概念。如果对象的赋值算是一个链条,那么 low-level const 的对象会对其下游(左边)对象有同样的限制。只要下游存在修改 low-level const 对象的风险,那么编译器就会报错。 ===Constexpr=== 这一节有一个概念,**常量表达式**(//Const Expression//)。 常量表达式是指在**自身值无法改变**,且**初始化值在编译阶段就可以确定**的表达式,比如 literal、被常量初始化的 const 对象等等。\\ \\ 因此,所有非常量的表达式,或者需要在 //Run Time// 下才能确定初始值的表达式(比如函数),都不是常量表达式。比如: int a = 10; // a is not const object const int b = getsize(); // getsize() fincution is an uncertain initializor during the compling time. const int c = 100; // const expression ==Constexpr Variables== 因为在大型系统中,判断一个表达式是不是 ''const'' 比较麻烦--你觉得你定义的常量是被“常量表达式”初始化的,但往往并不是。为了确保得到真正的常量表达式,C++11 新标准中提出了一个关键字 ''constexpr'' 。''constexpr'' 会要求编译器验证表达式是不是常量表达式。被 ''constexpr'' 修饰的变量定义表达式,必须满足常量表达式的两个特性,即: * 声明的变量必须是明确的 ''const'' (Top const) * 该变量必须被一个常量表达式初始化。 //A constexpr specifier used in an **object** declaration or non-static member function (until C++14) **implies const**. A constexpr specifier used in a **function** or static data member (since C++17) declaration **implies inline**.// constexpr 的优势主要在于能将初始化的过程放到编译期,而不是在 run-time 期让程序自己计算,这样可以提高客户端程序的效率。但这样带来的弊端就是定义下来的量是绝对无法更改的(除非重新编译)。因此,constexpr 才对定义的过程有要求:只有一个不可更改的,并在编译期就可以计算出结果的变量,才称之为 constexpr 变量。 ==Limitation of constexpr== 因为常量表达式的初始值需要在编译期确定,因此 constexpr 的对象的初始化会有一些局限性: - ''constexpr'' 只接受 //Literal Types// (比如算术类型和复合类型) - ''constexpr'' 不支持校验自定义对象类型(比如类) - ''constexpr'' 指针不能指向函数内定义的变量(地址不确定) ==指针与 constexpr== 在定义 constexpr 指针的时候,constexpr 特指指针的类型(top-level),而不是指针指向对象的类型: const int *p = nullptr; // p is a pointer to const int constexpr int *q = nullptr; // q is a const pointer to int 愚见:从结果上来讲,constexpr pointer 与 const pointer 对指针自身的限制都是一样的:都定义了一个无法更改指向地址的指针。但 const pointer 指向的地址中的内容是可以在 run-time 时期改变的;constexpr 只是确保初始化在编译期内进行。如果能确定指针指向的初始值全程不变,比如 const int *const p 这样的形式,完全可以用 constexpr 替代。\\ \\ 从这个角度来看,实际上绝大部分情况下,如果只是对指针本身的不可更改性有要求,那么 constexpr 是可以取代 const的;如果仅仅是要求无法通过指针来修改某对象,那么就使用 const。也就是: * top + low level 使用 constexpr * low-level 使用 const * top-level 酌情使用 ''const int *const p'' 这样的形式换成 constexpr 来定义可以写成这样: constexpr const int *p = &i; Ref:[[http://www.cprogramming.com/c++11/c++11-compile-time-processing-with-constexpr.html|Constexpr - Generalized Constant Expressions in C++11]] ====类型处理==== 类型随着程序的复杂性变大会出现两个比较显著的问题: * 类型名会变得越来越长,变得难懂并且容易输错 * 类型的形式太复杂导致需要看完类型的内容才能明白 ===Type Alias=== 解决第一类问题可以用到 //Type Alias//。 总的来说,//Type Alias// 就是为某种类型找一个同义词,通过简化复杂的类型使其更为易用。\\ \\ 有两种方法可以定义 //Type Alias//: - ''typedef'',格式为: typedef + 被定义的类型 + 同义类型 typedef double wages; // wages is a synonym for double'' typedef wages base; //base is synonym for double typedef wages *p; // p is synonym for double* - C++11的新标准:''using'' using SI = Sales_items;// Si is a synonym for Sales_items SI item; // same sa Sales_item type ==Pointer, const and Type Aliases== //Type Alias// 也可以与复合类型,''const'' 一起使用,但需要注意的是,''const'' 修饰的是 //Type Alias//。因此在与指针连用的时候要注意,''const'' 修饰的永远是 //Type Alias// 中的变量类型,比如: typedef char *pstring; const pstring cstr = 0; // const 修饰的是经过type alias化的指针 // const pstring -> const char *,cstr 是指向 char 的常量指针 // 而不是指向常量 char 的指针(char const*) ===Auto Type Specifier=== 在很多时候,我们想自己决定表达式的类型是非常困难的。C++11引入了一种新的类型说明符 ''auto'',让编译器自动决定表达式的类型。因为编译器需要根据变量的初始值来判断类型,所以 ''auto'' 声明的变量必须初始化。\\ 和别的说明符一样,''auto'' **定义多个变量的时候,所有变量必须是同一类型**。\\ ==Auto、复合类型、Const== ''auto'' 与复合类型还有 const 连用的时候,有几个点需要注意:\\ \\ 首先,使用引用对某类型进行初始化的时候,''auto'' 会将该类型推断为**引用 refer to 的类型**: int = 0, &r = i; auto a = r; // a is an int 其次,在有 const 参与的类型初始化中,''auto'' 会自动**忽略** top-level const: const int ci = i, &cr = ci; auto *p = &ci; // p points to a const variable, low-level auto b = i;// b is an int auto c = cr; // c is an int auto d = &i; // d is an int* 如果需要保留 top-level const, 需要**显式的添加上** const: const int i = 0; const auto a = i; 除此之外,low-level const 不会被 ''auto'' 忽略,因此 auto **不会**将 low-level const 的引用类型识别为其绑定的类型: const int ci = i; auto &g = ci; // g is an reference refer to const int ci auto &h = 42; // error, plain reference can be bound to a literal const auto &j = 42;// reference to const can be bound to literal ===Decltype Type Specifier=== ''decltype'' 与 ''auto'' 类似,也用于自动判断变量的类型。与 auto 不同的是,decltype 是通过**表达式**在**编译期**推断出变量的类型。因为 ''decltype'' 并不要求执行表达式,因此不需要初始化。那么有时候我们想让编译器自己推断一个表达式的类型,但是又不想用这个表达式做初始化,这时候我们就要用 ''decltype'' 了。''decltype'' 的用法如下: decltype(f()) sum = x; //sun has whatever x returns 跟 ''auto'' 不一样,**变量是什么类型,''decltype'' 就返回什么类型**,包括 //top_level// ''const'' 和 引用类型: const int ci = 0; &cj = ci; decltype(ci) x = 0; //x is const int decltype(cj) y = x //y is reference to const int x decltype(cj) z; //error, exception; if z requires initialize, then the initialization is a must. 注意 decltype 与引用连用的时候需要初始化(因为引用需要)。与 auto 不同的是,decltype 会返回其表达式的类型,因此这里返回的是引用,所以必须初始化。 ==Decltype 和 表达式== decltype 的返回类型是根据表达式的表现形式来判断的。我们设定 ''e'' 的类型为 //T//; - 如果表达式是函数,那么返回的就是函数返回值的类型 - 如果 ''decltype(e)'' 中,e是一个实体(//Entity//)的名字(id 表达式),那么 ''decltype'' 返回的是名字为 ''e'' 的实体(//Entity//)的类型。 - 如果 ''e'' 是表达式: -如果表达式 ''e'' 是个左值,''decltype(e)'' 返回的就是 //T&//(//T// 的引用),注意:如果有括号,**括号里的表达式将被视为左值**。 -否则,返回的是 ''e'' 的返回结果的类型。 Refs: * [[http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf|C++11 standard P153 section.4]]\\ * [[http://en.cppreference.com/w/cpp/language/decltype|decltype specifier]] * [[http://stackoverflow.com/questions/3601602/what-are-rvalues-lvalues-xvalues-glvalues-and-prvalues|What are rvalues, lvalues, xvalues, glvalues, and prvalues?]] 一些例子: int i = 10; int &r = i; int *p = &i; decltype(i) x1 = 0; //decltype ( entity )形式,返回 i 的类型int decltype(r) x2 = x1;//decltype ( entity )形式,返回绑定int变量的引用(r绑定的是i) decltype(r+0) x3;//decltype ( expression )形式,r+0的结果类型是int,但r+0不能作为lvalue,所以返回int decltype(*p) x4 = i;//decltype ( expression )形式,*p是lvalue,那么返回的是T& .*p表示的是int变量i的值,那么返回的类型就是对int变量的引用。 decltype((i)) x5 = i;// if the name of an object is parenthesized, it is treated as an ordinary lvalue expression.(i)是lvalue,那么返回T&,也就是int的引用。 decltype('a') x6;//e是prvalue(pure value),返回e自身的类型。 赋值表达式也是一种左值,因此会返回引用。引用的类型由赋值操作符左边的变量决定。比如 decltype(i=0) 会返回int&。 ====Defining Our Own Data Structures==== 一些注意事项: * class 定义的末尾要加一个 semicolon * object 的定义最好与 class 的定义分开 ===Header=== * 因为 class 的复用性,因此 class 的定义通常应该放到 header 中。 ==使用Preprocessor解决多重包含== Preprocessor 继承至 C,指一系列在编译器改变源代码之前运行的程序,比如 ''#include''。Preprocessor 有一个用途是解决多重包含。\\ \\ 在我们的 C++ 程序中,资源往往来自不同的头文件,某一些头文件之间可能会有互相的依赖。比如我们自定义的头文件 ''Sales_item.h'',为了输出字符串,必须包含 ''string'' 头文件。如果在接下来的某个源文件中需要同时使用 ''Sale_item'' 和 ''string'' 中定义的资源,那么该文件就会多重包含 ''string'' 头文件。 \\ \\ **//多重包含会带来什么问题?//**\\ \\ C++ 中,某对象的声明可以有很多次,定义只能有一次。多重包含往往会导致某个对象的重复定义,这是不被允许的。 \\ \\ 为了解决这个问题,C++ 提供了 //Header guards//。//Header guards// 使用 preprocessor 提供的变量来确保当前 header 只被包含一次: #ifndef SALES_DATA_H //检测之前该 header 有没有被定义(包含) #define SALES_DATA_H //如果 header 是被第一次包含,那么证明该 header 未被定义过,执行下列语句 #include struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; #endif 其中 ''#define'' 用于存储当前变量的状态,有两种情况:定义或者未定义。而 ''#ifndef / #ifdef - #endif'' 字符段用于判断当前的变量是否被定义。''#ifdef'' 表示如果当前变量已经定义进行下一步, ''#ifndef'' 表示当前变量没有被定义则进行下一步。因此上边代码实际上会这么运行: * 如果当前程序第一次包含 ''Sales_data.h'' 头文件,#ifndef 测试成功,对 ''SALES_DATA_H'' 进行定义,并拷贝 ''Sales_data.h'' 到目标程序。 * 如果已经包含了 ''Sale_data.h'' 头文件,那么 #ifndef 测试会失败,那么代码段之间的内容会直接被忽略掉。 需要注意的是,Preprocessor 中的所有变量,必须是**唯一**的,并且必须**全大写**(避免与其他名字发生冲突)。 所有的 Header 文件都应该使用 header guards。