《C++ Primer》1. C++ 基础

变量和基本类型

基本内置类型

关于 charsigned charunsigned char

  • char 被当成有符号或是无符号视不同编译器决定
  • 在定义数值类型时使用 signed charunsigned charsigned char 表示 -128 到 127,unsigned char 表示 0 到 255

类型转换:

  • 浮点数赋值给整数时,截断小数部分
  • 整数赋值给浮点数时,小数部分记为 0,如果整数所占空间超过浮点类型的容量,精度可能有损失

含有无符号类型的表达式:

如果算术表达式中既含有无符号数又有 int,那 int 会转换成无符号数,如:

unsigned u = 10;
int i = -42;

std::cout << i + i << std::endl; // -84
std::cout << u + i << std::endl; // 4294967264

字面值:

  • 整型:十进制、八进制(024)、十六进制(0x14
  • 浮点型:3.14153.1415e100..01
  • 字符和字符串:转义

可以添加前后缀来指定字面值的类型:

  • 宽字符 L'a'
  • utf-8 字符串 u8"Hi!"
  • 整型:42ull42u32ull
  • 单精度浮点:1e-3f
  • long double3e-10L

如果使用列表初始化时,存在信息丢失的风险,那么编译器会报错

复合类型

引用、指针

注意指针解引用后是引用类型

从右向左阅读,如 int *&r 是对 int * 的引用

NULLnullptr

  • NULL 是个预编译宏,nullptr 是个编译期常量
  • NULL 的类型是 void *(C)或者整型(C++),nullptr 的类型是 nullptr_t
  • 模板和函数重载时,NULL 可以匹配为整型,而 nullptr 不会

常量

  • 常量引用
  • 常量指针与指向常量的指针(const 放在 * 后面)
  • 底层 const 与顶层 const:本身不能改变的是顶层,本身可以改变但是指向的值不能改变的是底层,引用是底层 const,因为引用本来就是不能改变的
  • 底层和顶层不止可以说指针,任何变量都可以这么讲

其实可以这么区分:* 左侧是底层,右侧是顶层

const int x;         // 顶层
const int * x;       // 底层
int * const x;       // 顶层
const int * const x; // 顶层 + 底层

const int& r;        // 底层
const int&& r2;      // 底层

constexpr 与常量表达式

  • 可以将变量声明为 constexpr 来让编译器验证该变量的值是否是常量表达式
  • constexpr 还可以声明函数
  • 声明为 constexpr 时使用的类型必须为字面值
  • constexpr 可以修饰引用,此时不能指向函数内的变量
  • constexpr 声明的指针不分底层和顶层,只对指针有效,与指针指向的对象无关(只有顶层),constexpr 指针可以指向常量也可以指向非常量

字面值类型:

  • 算术类型、引用和指针都属于字面值类型
  • string 类等不属于字面值类型

typedef 与常量指针

#include <iostream>
using namespace std;

typedef char* pstring;

int main() {
    pstring s1;
    const pstring s2 = "323";
    
    return 0;
}

上面的代码中,s1char * 类型,s2char * const 类型,其中 const 修饰的是指针,它是个顶层 const

auto 类型说明符

  • auto 修饰变量时必须有初始值,如果一条声明语句声明多个变量,那么它们的类型必须相同
  • auto 推断出的类型和初始值的类型并不完全一样,有一些规则:
    • 当引用被当做初始值时,使用引用对象的类型
    • 一般会忽略顶层 const,同时底层 const 会保留下来,如果需要保留顶层 const,那么需要添加 const 修饰符
    • 在引用时同样

可以说,auto 在推断类型时总是退化的(左值到右值、数组到指针、函数到函数指针,并且去除 cv 限定符)

int i = 0, &r = i;
auto a = r;                      // int

const int ci = i, &cr = ci;
auto b = ci;                     // int
auto c = cr;                     // int
auto d = &i;                     // int *
auto e = &ci;                    // const int *

const auto f = ci;               // const int

auto &g = ci;                    // const int &
auto &h = 42;                    // error
const auto &j = 42;              // const int &

decltype 类型指示符

选择并返回操作数的数据类型,分析表达式并得到类型,但是并不实际计算表达式的值

decltype 处理引用和顶层 const 的方法与 auto 不同,它直接返回引用和 const

注意,引用从来作为其所指对象的同义词出现,只有在 decltype 处是个例外

加不加括号也会影响 decltype 的类型:

  • 不加括号表示变量
  • 加括号表示表达式,由于变量是可以作为左值的特殊表达式,所以会返回引用
const int ci = 0, &cj = ci;

decltype(ci) x = 0;    // const int
decltype(cj) y = x;    // const int &

int i = 42, *p = &i, &r = i;
decltype(r + 0) b;     // int
decltype(*p) c;        // int &,没有初始化

decltype((i)) d;       // int &,没有初始化
decltype(i) e;         // int

表达式

左值与右值

C++ 的表达式要不然是右值(rvalue),要不然就是左值(lvalue)

当一个对象被用作右值时,用的是对象的值(内容);当被用作左值时,用的是对象的身份(在内存中的位置)

一个基本原则(有一个例外):在需要右值的地方可以用左值替代,反过来不行

运算符使用和返回的值类型:

  • 赋值运算符:被赋值的必须是左值,结果是左值
  • 取地址符:需要左值,结果是右值
  • 内置解引用运算符、下标运算符、迭代器解引用运算符、很多容器的下标运算符:结果是左值
  • 内置类型和迭代器的递增递减运算符:需要左值,前置版本结果是左值,后置版本结果是右值

使用 decltype 时,左值和右值也有所不同:

  • 如果表达式的求值结果是左值,那么会返回引用
  • 如果是个纯右值表达式,则返回非引用

运算符与运算顺序

int i = 0;
cout << i << " " << ++i << endl;

上面的代码中,是个未定义行为

位求反运算符:反引号,所有位都求反

sizeof 运算符

  • 指针:返回指针本身大小
  • 引用:返回被引用对象大小
  • 解引用指针:指针指向对象所占空间大小,指针不需要有效
  • 数组:整个数组所占空间大小,等价于对数组中所有元素各执行一次 sizeof 并求和
  • string 对象或 vector:固定部分的大小,不会计算对象中的元素占用了多少空间

类型的隐式转换和显式转换

算术转换:

  • 整型提升
  • 有无符号类型:
    • 如果两个都是有符号或者都是无符号,那么选大的
    • 如果一个有符号,一个无符号,且无符号的位数不小于有符号,那么选择有符号
    • 如果一个有符号,一个无符号,且无符号的位数小于有符号,那么结果依赖于机器(?)

还有其他类型的隐式转换:

  • 数组转换成指针
  • 指针可以转换成 const void *
  • ifwhile 中转换成 bool 类型
  • 转换成常量
  • 类的单参数构造函数

显式转换:

  • static_cast
  • const_cast
  • reinterpret_cast:非常危险

语句

switch 语句内部的变量定义:

如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为

case true:
    string file_name;                // 错误,控制流绕过来一个隐式初始化的变量
    int ival = 0;                    // 错误,控制流绕过来一个显式初始化的变量
    int jval;                        // 正确,它没有被初始化
    break;
case false:
    jval = next_num();               // 正确
    if (file_name.empty()) { ... }   // 如果绕过了上一个 case,那么这个变量就没有被初始化,造成缺陷

我们可以嵌套一个大括号来嵌套一个作用域:

case true:
    {
        ...
    }
    break;

函数

函数声明、形参、实参、值传递、引用传递

不允许拷贝数组

数组形参:

  • int * arr[10] 表示 10 个指针构成的数组
  • int (* arr)[10] 表示指向含有 10 个整数的数组的指针,用来表示多维数组
  • int arr[][10] 与上面等价

initializer_list 形参

省略符形参(C 语言中的东西):

void foo(parm_list, ...);
void foo(...);

如果函数返回值是引用类型,那么它表面函数返回一个左值

返回指针数组时的声明:

int (* func(int i))[10];

它表示输入是 int,返回一个第二维大小为 10 的二维 int 数组

或者使用后置类型,写成:

auto func(int i) -> int(*)[10];

如果我们有一个这种类型的数组的话,可以使用 decltype

默认实参

  • 作用域内一个形参只能被赋予一次默认实参
  • 函数的后续声明只能为之前那些没有默认值的形参添加默认实参,并且该形参右侧所有形参都必须有默认值
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*');       // 错误:重复声明
string screen(sz = 24, sz = 80, char);   // 正确:添加默认实参

局部变量不能作为默认实参:

int main() {
    int h = 10;
    string screen(int x, int y = h);     // 错误,使用了局部变量 h
}

内联函数和 constexpr 函数

  • 返回类型和形参的类型都得是字面值类型
  • 函数体中的语句在运行时不执行任何操作
  • 调用时,当参数都是 constexpr 时返回值就是 constexpr

内联函数和 constexpr 函数都是内部链接性

调试

assert

NDEBUG 宏:是否处于 debug 模式

__func__ 宏:当前调试的函数名称

__FILE____LINE____TIME____DATE__

函数匹配

确定候选函数:

  • 与被调用的函数同名
  • 声明在调用点可见

从候选函数中确定可行函数:

  • 形参数量与实参数量相同(默认实参除外)
  • 形参类型与实参类型相同,或者实参类型能够转换成形参的类型

寻找最佳匹配:

实参类型与形参类型越接近,它们匹配地越好。对于多个形参,如果有且仅有一个函数满足条件:

  • 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
  • 至少有一个实参的匹配优于其他可行函数提供的匹配

那么选择它,否则出现二义性

为了确定最佳匹配,编译器划分了几个等级:

  • 精确匹配:
    • 实参类型与形参类型相同
    • 实参从数组类型或函数类型转换成对应的指针类型(也就是 int * aint a[] 等价)
    • 向实参中添加顶层 const 或从实参中删除顶层 const(例如 func(const int x) 可以被 int x = 10; func(x) 匹配)
  • 通过 const 转换实现的匹配(这里指的是底层 const,例如 const int *int *
  • 通过类型提升实现的匹配
  • 通过算术类型转换(除去类型提升之外的算术类型转换)或指针转换(任何指针都可以转换成 const void *,有继承关系的类之间指针的转换)实现的匹配
  • 通过类类型转换实现的匹配

函数指针

函数指针只需要将函数声明中函数名前面加上 * 并加上括号即可

在调用时可以加 * 也可以不加

例如:

int (* func(int i))[10] {
    cout << "func(int i)[10]" << endl;
    return nullptr;
}

int main() {
    int (* (* pfunc)(int i))[10] = func;
    
    pfunc(10);
    (*pfunc)(10);
    return 0;
}

当使用重载函数时,必须精确的界定使用哪个函数:

void ff(int *);
void ff(unsigned int);

void (* pf1)(unsigned int) = ff;        // 正确
void (* pf2)(int) = ff;                 // 错误,不能匹配到任何一个函数
double (* pf3)(int *) = ff;             // 错误,返回值不匹配

函数指针和函数类型可以作为形参,但是它们等价,都表示一个函数指针:

void useBigger(bool pf(const string &, const string &));
void useBigger(bool (* pf)(const string &, const string &));

上述两个函数等价

函数指针也可以作为返回值(函数类型不行):

using F = int(int *, int);
using PF = int(*)(int *, int);

PF f1(int);    // 正确
F f1(int);     // 错误,函数类型不能作为返回值
F * f1(int);   // 正确

int (* f1(int))(int *, int); // 与上面等价

友元

友元不具有传递性,即如果 BA 的友元,CB 的友元,不代表 CA 的友元

如果一个类想把一组重载函数声明成它的友元,那么它需要对这组函数中的每一个分别声明

名字查找与类的作用域

类的定义分为两阶段来处理:

  • 处理成员的声明
  • 在类全部可见后才编译函数体

这样的处理方式使得类成员可以使用类的定义中的任何名字,也带来了限制

如果类的成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字,如:

typedef double Money;

class Account {
public:
    Money balance() { ... }    // 使用了外层的 Money
private:
    typedef double Money;
    Money bal;                 // 错误:不能重新定义 Money
};

注意即使两个 Money 类型一致,这也是错误的

构造函数

构造函数初始化列表中的成员的初始化顺序:与类定义的出现顺序一致

下面的写法会导致缺陷:

class X {
    int i;
    int j;
public:
    X(int val) : j(val), i(val) { ... } // i 在 j 之前被初始化
};

最好让初始化成员列表的顺序与类成员顺序保持一致

假如有委托构造函数,那么先执行委托构造函数的初始化,然后控制权才交给原来的构造函数

注意默认构造函数使用时:

ClassName obj1();   // 错误,它定义了一个函数
ClassName obj2;     // 正确

转换构造函数只允许隐式转换一步

可以使用 static_cast 来代替显式的转换构造函数调用

字面值常量类:

  • 数据成员必须都是字面值类型
  • 类必须至少含有一个 constexpr 构造函数
  • 如果一个数据成员含有类内初始值,那么内置类型成员的初始值必须是一条常量表达式
  • 如果数据成员属于某种类类型,那么初始值必须使用成员自己的 constexpr 构造函数
  • 类必须使用析构函数的默认定义

constexpr 构造函数必须既符合构造函数的要求,又符合 constexpr 函数的要求

静态成员:

  • 通常情况下,不应该在类的内部初始化
  • 静态成员(和指针成员)可以是不完整类型,而其他的数据成员必须是完整类型
  • 静态成员可以作为默认参数