======对象与基本类型====== //第 2 章笔记// ---- ====初始化与赋值==== * 初始化:将某个值与某个对象关联起来 * 在内存中开辟空间,保存数值 * 在编译器中构造变量的**符号表**,将其与对应的值关联起来 * 赋值:将某个已关联的变量关联到新的值 * 不需要开辟新的空间和构造符号表 * 初始化和类型可能会涉及类型转换 ==value== * 字面值(//literal//):一眼就能看出来是值的,比如 ''10'' * 对象:变量、常量,或者引用。通过标识符定义。比如 ''x = 10'',''x'' 就是对象 * 值和对象都有**类型** ====类型详述==== * 类型是编译器概念,是在编译阶段检测的。引入类型可以更好的描述程序,防止误用。 * C++ 是强类型语言 ===类型描述的信息=== * 存储的尺寸: * 使用 ''sizeof()'' 测量 * 大小与编译环境有关系,C++ 不做限制。 * 取值空间: * 使用 ''std::numeric_limits'' 查找,范围为 $2^{bit length}$ * 超过取值空间的值会溢出,跟二进制码有关 * 对齐信息:CPU 读写数据会读到 cache 中。cache line 会有大小(32 / 64 位),也就是每次读取 ''32'' 或者 ''64'' 位数据。如果存储的数据位置处于两次读取位置的**之间**,那么需要两次读写才可以完整的读取存取的数据。这种情况被称为内存对齐不良。这种情况可以通过安排数据的位置进行改良。 * 查看类型的对齐信息:''alignof(type)'' * 对齐信息为了保证对其,会影响结构体的大小。 * 类型可以执行的操作 #include int main(int argc, char const *argv[]) { int x{0}; std::cout << "Size of: " << sizeof(x) << std::endl; std::cout << "Int min: " << std::numeric_limits::min() <::max() < ====类型==== ===基本(内建)类型=== * 数值类型: * 字符类型(''char'', ''whcar_t'', ''char16_t'', ''char32_t''):char 以外的 char 是针对其他编码的特殊字型的;比如 Unicode * 整数类型 * 带符号整数:short, int, long, longlong * 无符号整数:unsigned,只能取正数 只有 ''unsigned'' 指代 ''unsigned int'' * 浮点类型 * float, double, long double:double 的精度更高,但占用空间更大,计算的代价也更高 * void 类型 ===复杂类型=== * 由基本类型组合生成的类型 ==未定义部分== * C++ 中并未指定 char 是 signed 还是 unsigned * 带符号:[-128, 127] * 不带符号:[0, 255] * 解决方案:直接指定 char 是否有符号 unsigned char c1; signed char c2; * 整数在内存中的表示方法未确定: * 不同机器之间传输需要确定保存方式为**大端还是小端** * C++ 中类型的大小不确定 * 为了最大限度利用硬件的处理,但会影响程序的可移植性 * C++11 中提供了固定大小的类型:比如 ''int32_t'' (32 bit, 4 byte) (//fixed width integer types//) ===字面值=== * 字面值:在程序中表示为一个具体的数值或者字符串的值 * 整数字面值(''int''): 十进制 / 8 进制 / 16 进制 * 浮点字面值: * 普通写法:''3.14'' * 科学计数法: e 后的数字代表了乘以 e 的多少次方(一定是 double) * **字符**字面值:**单引号**内的内容,一定是 ''char'' 类型,比如转义字符 ''\n'', ''\x4d'' * **字符串**字面值:**双引号**内的内容,是 ''char[]'' 类型,以 ''\0'' 作为结尾。因此,''Hello'' 的长度是 ''6'' 位,而不是 ''5'' 位。 * 布尔字面值:''true'' / ''false'' * 指针字面值:''nullptr'',''nullptr_t'' 类型 ==指定字面值的类型== // 指定 float 类型的字面值 float x = 1.3f; // 指定一个 unsigned long long 类型的字面值 unsigned long long y = 2ULL; ==用户自定义的字面值== C++ 11 允许用户自定义字面值的类型。该定义通过重写运算符 %%""%% 进行定义。该功能可以方便我们定义一些单位(比如时间 / 重量)等等。下面的字面值代表了将接收到的 double 乘以 2 再转化为 int 的结果 int operator "" _customType(long double x) { return (int)x * 2; } int main(int argc, char const *argv[]) { //using _customType as an literal type int x = 3.14_customType; std::cout << x << std::endl; } 需要注意的是,User-defined literals 能接收的参数类型是有限的(引入的函数接收的类型是有限的)。 * Ref:[[https://en.cppreference.com/w/cpp/language/user_literal|User-defined literals]] ====变量及其类型==== ===变量的声明与定义=== * 变量的类型在**首次声明**的时候确定 * 变量的定义和声明的区别: * 定义需要为 name 分配具体的空间 ==extern keyword== ''extern'':实现的是定义只有一处,声明可以有多处的概念,用于外部文件引用实现文件中的定义。\\ \\ //**为什么要使用 extern 关键字?**// \\ \\ 假设我们有两个文件需要一起编译。A 中使用调用 B 中已经定义的变量 x。如果没有 ''extern'',那么会出现两种情况: * A 中没有定义 x,B 中定义了 x:此时会报编译错误,因为 x 对 A不可见 * A 中定义了 x, B 中也定义了 x:此时会报链接错误,因为 A, B 中的 x** 重定义**。 因此,如果希望在 A 中使用 B 中定义的 x,那么必须使用 ''extern'' 关键字。''extern'' 会让编译器将定义视作外部文件变量在本地的声明,从而达到我们希望的效果: //source.cpp int x; //main.cpp extern int x; int main() { std::cout << x << std::endl; } extern 引入的变量如果进行初始化,那么就会从声明转化为定义。 ===变量的初始化和赋值=== * 初始化:构造变量之初为其赋予一个初始值 * 赋值:改变值 ==初始化的方式== * 默认初始化: * 算术类型: * 函数外部(全局):''0'' * 全局变量只会在程序开始之初进行初始化,因此代价并不昂贵。 * 函数内部:undefined * 为函数内部的变量设置默认值是有消耗的。在不清楚函数被调用多少次的情况下,提供该项服务是不利于性能的。 C++ 只会提供一块内存给该变量,但不保证该变量的内容。 * 直接 / 拷贝初始化 * List 初始化:强制检查类型 //direct init int x(10); //copy init int x = 10; //list init int x{10}; ===(隐式)类型转换=== * promotion (小类型转化为大类型) 不会导致数据的损失 * signed 和 unsigned 之间的转换:都会转换为 unsigned * bool 与整型的转换 * 除了 0 以外的整数的 bool 值都是 1 * bool 转换为整数结果为: 1(true) 和 0(false) * 隐式转换会出现在其他地方: * 条件判断时 * 数值比较时(之前提到的 signed 和 unsigned 情况) * 牵涉不同类型数据比较,可以使用 ''std::cmp_XXX'' 库(C++ 20) ====复合类型==== ===指针=== * 间接类型:开辟了一个空间,存储的是指定内存序列的**第一个地址** * 可以指向不同的变量 * 所有指针的尺寸都是相同的 ==指针的定义== //p points to the address of the val int* p = &val; ==指针的操作== * 取地址 ''&'' * 解引用 ''*'':不能访问没有内容的地址(''0'' 地址)一般情况下会给一个 ''0'' 地址来保证错误的解引用一定会报错: // 指针指向内容随机,解引用结果无法预测 int *p; // null pointer // 访问报错,但是内容稳定,方便查错 // 缺点是存在整型到指针的隐式转换 int *p = 0; int *p = NULL; // 防止隐式转换,C++ 11提供的 nullptr 指针对象 int *p = nullptr; * 自增自减 ''++ / --'':移动到下一个(上一个)指向的位置,距离取决于指针指向变量的类型 * 指针相等''=='':判断两个两个指针是否指向同一个内存单元 * 比较大小:只能在两个指针指向同一个数组时使用,判断两个指针的位置 ==void* 指针== * 尺寸与普通指针相同,可用于验证传入的数据类型是否是指针(判断两个指针是否指向同样的对象) * 可以转换为任意类型的指针,通常会作为接口参数使用 * 没有指向内容的占位大小信息,因此不能使用指针的算术运算 ==指向指针的指针== int x = 42; int *p = &x; int **pp = &p; 指针是对对象的间接引用: * 其好处在于可以减少程序**传递**的成本。这点在复制比较大的对象上尤其明显。其次,某些对象不可复制,因此只能通过复制其地址来达到复制的效果。 * 但带来的缺点是读写成本高:因为读写之前都需要解引用 * 指针可以修改指向对象的值: * 普通情况下,argument 传递给 parameter 是通过值(拷贝)传递,修改 parameter 的值不会影响 argument * 传指针时,argument 和 parameter 指向的是同样的内存地址,因此修改 parameter 也会导致 argument 的改变 ==指针与 bool 的隐式转换== * 空指针(指向 0 地址) ''false'' * 反之为真 * 通常使用 ''if (p)'' 方式来书写条件判断 ===引用=== 指针可能存在的问题:指向的对象为空或者非法。采用引用是另外一个较好的选择。引用是变量的**别名**: int x = 42; //& is an type modifier here int &refX = x; * 字面值没有地址,因此不能使用引用绑定字面值 * 引用一旦绑定后,在声明周期类不可再更改(指针可以重新指向别的 name,引用不行) int x = 0; int y = 1; int *ptr = &x; //打印 ptr 结果相同,但实际上是不同的操作 //改变 x 所在内存的值为 y, 1 in x *ptr = y; //改变了 ptr 指向的位置,从 x 到 y,1 in y ptr = &y; 从上面的例子来看,引用**只能改变其指向内存地址中的内容**,而不能改变其指向的位置。这要求引用在初始化的时候**必须绑定一个实际的对象(的地址)**。 需要注意非法引用的问题:通过函数返回局部变量的引用可能出现问题。 * 底层还是通过指针来实现 ==指针的引用== * 指针是对象,因此存在指针的引用: * 引用不是对象,因此不存在引用的引用 int* &refPx = ptr; ====常量与常量表达式==== * 常量:不可修改的对象 * 可以被视作由编译器保证其不被修改的变量 * 防止非法操作:比如条件判断时可以避免误写 ''='' 代替 ''=='' 带来的问题 * 编译器可以优化:常量不需要每次都判断(读取)值,但变量需要 ===常量指针与顶层常量=== 指针与常量结合时,需要考虑哪个部分不能被修改: * 指针本身 * 指针指向的部分 如果限制的是指针本身,那么: // 指针本身无法改变 int* const ptr = &x; 如果限制的是指针指向的内容:,那么: //指针可以改指其他位置,不能通过该指针改变其指向的内容 const int* ptr = &x; ''const'' 出现在 ''*''** 左边**代表指针本身不能指向其他地方。 ==顶层常量== * 顶层常量限制的是常量本身无法被修改 * 底层常量限制的是指向的内容不能修改 ==底层常量的隐式转换== * 读写 -> 只读是可以的 // int * to const int * int x = 4; // &x 由 int* 转化为了 const int*,该指针之前可以对 x 进行读写,但现在只能读取 x const int *ptr = &x; * 只读 -> 可写是不行的 // const int * to int * const int x = 4; // error, &x 无法写 x,因此 ptr 也不能对 x 进行写操作 // low-level const 是有传递性的,如果之前只读,那么之后也必须只读(不管通过什么样的途径,比如赋值,参数传递等等) int* ptr = &x; // good const int* ptr = &x; ==常量引用== * const int&:无法通过该引用修改其绑定的值 * 主要是用于高效的函数传递参数: * 传递某个大对象,但又不想改变该对象,就用 const int& * 传递某些无法拷贝的对象 * 相比指针,引用不需要判断参数的有效性(空指针什么的) * built-in 数据不需要使用引用,值传递更效率一些 * **常量引用可以绑定字面值**: * 常量引用通常作为函数的 parameter,当函数采用字面值作为函数参数的时候,需要保证引用可以绑定该字面值,才能完成调用。 ===常量表达式=== 不同的常量确定的时机不同。比如下面的例子: int y; std::cin >> y; // y1 是在运行期确定的(运行期常量) const int y1 = y; // y2 是在编译期确定的(编译期常量) const int y2 = 3; 由于这样的问题,编译器在处理 ''y1'' 和 ''y2'' 时,就会有不同的方式。比如以上述的变量做条件判断 ''if (y1 == 3)'': * ''y1'' 会以“期待运行期有输入的”方式进行汇编 * ''y2'' 对编译器可见,那么 ''y2 == 3'' 一定为真。因此,编译器不会进行 ''if'' 的判断,而会直接执行该 if 下的代码段。(优化) 也就是说,编译器常量是可以被编译器优化的。这也是常量表达式的由来:C++ 11 中通过 ''constexpr'' 关键字,将常量直接声明为**编译期期量**。也就是说,上述的 ''const int y2 = 3'' 可以写为: // y2 被显式的声明为编译期常量 // y2 的类型是 const int // constexpr 是一种带指导意义的限定符(//specifier//),不属于类型声明的一部分 constexpr int y2 = 3; ==常量表达式指针== 指针也可以作为编译期常量。这种情况下,需要满足两个条件: * 指针是常量 * 指针指向的内容也是常量 只有满足这两个条件,指针才能作为编译期常量对编译器可见。也就是说,这种情况下的指针类型是 ''const type* const'' constexpr const int* ptr = nullptr; //字符串 constexpr const char* str = "abc"; 可以使用 std::is_same_v 来判断类型是否相同。定义于 头文件 ====类型别名与自动推导==== ===类型别名=== 类型可以引入别名,便利使用(比如 ''size_t'' 实际上是一个 unsigned int)。别名的引入有两种方式: * typedef + type + alias * using alias = type //myInt 是 int 类型的别名 //推荐使用 using //别名实际上是 myCharArr, 但 typedef 的引用看起来非常 confusing typedef char myCharArr[4]; using myCharArr = char[4]; ==类型别名与指针和引用== * 当使用别名定义指针时,别名代表了指针的整体。此时如果使用 ''const'' 修饰别名,那么 ''const'' 修饰的是指针,也就是将指针定义为**常量指针**。换句话说,下面两者等价: using intP = int*; int x = 3; //别名定义 const intP constIntPAlias = &x; //等同于 top-const pointer int* const constIntP = &x; * 不能使用别名来创建引用的引用 ===类型推导=== C++11 允许使用 ''auto'' 关键字让编译器自动通过**初始化表达式**的结果推断变量的类型: auto x = 3.5 + 15l; ==auto== * ''auto'' 推导出的类型是强类型(定义时已经确定) * 因为需要初始化表达式推导类型,因此自动推导下,表达式**必须初始化** * ''auto'' 可能会导致类型退化 int x1 = 3; int& ref = x1; //类型退化,ref 作为右值,此处由 int& 退化为 Int, 而不是 int& int y = ref; //auto 导致类型退化, ref2 是 int 类型 auto ref2 = ref; * ''const / constexpr'' 与 ''auto'' 组合会推导出常量 / 常量表达式类型 //const int const auto x1 = 3; //const int& const auto& x2 = 3; const int x3 = 3; //const int const auto y1 = x3; //int (top-const 的退化) auto z = x3; * auto 加引用会避免退化 //不会退化,const int& auto& y2 = x3; * 数组的推导会退化为指向数组首元素的指针 int x[3] = {1,2,3}; //int* auto x1 = x; //加引用不会退化:int(&)[3] auto& x2 = x; ==decltype== decltype 获取一个表达式,并返回表达式的类型。decltype 与 auto 的区别在于,decltype **不会产生类型退化**。 * decltype(val):''val'' 代表 entity(**变量名称**),那么 ''val'' 是什么类型,那么返回的就是什么类型 //x is an variable name int x = 3; //int decltype(x); //注意加括号的形式,(x) 是表达式, x 是变量名 //int& decltype((x)); * ''decltype(exp(r-value))'' :如果表达式是非变量名称的表达式,且为右值,那么返回的是表达式评估后的类型: int x = 3; int& y1 = x; //int auto y2 = y1; //int& decltype(y1) y3 = y1; * ''decltype(exp(l-vaule))'':如果表达式为非变量名称的左值(通常带运算符),返回的类型会自动加上一个**引用** int x = 3; int* ptr = &x; //*ptr 是包含了解引用操作符的表达式:此处是 l-value,此处得到的是 int& decltype(*ptr); * decltype(auto) [C++14]:用于简化 decltype 的使用 //非常繁琐的写法 decltype(3.5+15l) x = 3.5 + 15l; //C++ 14 的写法 // 用 auto 来代表繁琐的表达式,并不会导致退化 // 编译器会将 auto 替换为赋值运算符右边的内容 decltype(autol) x = 3.5 + 15l; * concept auto [C++20]:任何 concept 都会包含一系列类型,这些类型的共同特征都可以用 concept 表示。比如 ''int'', ''long'' 都可以表示为 ''std::intergal'' * concept auto 让自动推导受 concept 的范围限制,比如让 auto 只能推断出整数类型的类型: #include int main() { //int std::integral auto z = 3; //error, 3.5 is not an integral std::integral auto z = 3.5; } ====域和对象生命周期==== ===域 Scope=== 域代表了程序的一部分,域中的 name 有**唯一的含义**: * 全局域:程序最外部的域,全局对象 * 块域:大括号限定的域,局部对象 * 其他类型的域:Namespace, class 等等 域可以进行嵌套,内部域中的 Name 会掩盖外部域中的 Name: int x = 3; int main() { int x = 4; //call local x std::cout << x << '\n'; } ===对象生命周期=== 生命周期指对象从被**初始化到被销毁**的区间。 * 全局对象:生命周期为程序的运行期 * 局部对象:起始于**初始化**,结束于域的执行完成