C++基础总结

当程序某处使用一种算术类型的值而其所需的是另一种类型的值时, 编译器会执行相应的隐式转换

1
2
3
4
5
// unsigned和int混用
unsigned ui = 40;
int i = 10;
cout<< ui - i <<endl; // 30
cout<< i - ui <<endl; // 4294967266

初始化列表: 如果使用列表初始化但初始值存在丢失信息的风险(隐式转换), 那么编译器会报错

1
2
int i = 1.0; // warning C4244: 'initializing': conversion from 'double' to 'int', possible loss of data
int i = {1.0}; // error C2397: conversion from 'double' to 'int' requires a narrowing conversion

在函数体内的内置类型和指针的变量将不会被默认为初始化, 其值为未定义的, 使用这些数据将引发错误

1
2
3
4
5
6
7
int gi; // ok, gi = 0
void func(){
int i; // warning C4101: 'i': unreferenced local variable
// cout<< i <<endl; // error C4700: uninitialized local variable 'i' used
int *pi;
int *pj = pi; // error C4700: uninitialized local variable 'pi' used
}

引用和指针的区别

  • 引用和指针都是间接的访问对象, 选用引用还是指针取决于对象的状态. 如使用一个变量指向一个对象, 而变量在某些时刻可能不指向任何一个对象, 那么就应该把这个变量声明为指针, 如果能够确定该变量一定指向一个对象, 则可以把这个变量声明为引用.

  • 不存在指向空值的引用意味着使用引用的代码比使用指针的代码效率高, 因为不要验证引用类型的合法性, 但指针却总需要被验证, 防止其为空指针.

  • 指针可以重新赋值以指向另一个对象, 而引用则总是指向初始化时被指定的对象.

  • 引用的类型和被引用对象的类型应该严格一致

    但有两个例外:

    1. 初始化常量引用时允许用任意表达式作为初始值, 只要该表达式的结果能转换成引用的类型即可

      1
      2
      void func(int& i);  // func(12); oops! 非常量引用将报错, 需要建立引用的实体, 即int i = 12; func(i);方可
      void func(const int& i); // func(12); ok

      指针的类型也需要和指向的对象严格匹配
      有两个例外:

    2. 允许一个指向常量的指针指向一个非常量数据, 可以这么理解const并不影响变量的类型

    3. 指针本身是对象, 指针可以指向不同的对象, 指针勿需初始化, 但和内置类型一样, 块作用域内的指针如果没有被初始化, 其值未定义, 所以在创建指针时也请手动初始化.

      1
      2
      3
      int *p = nullptr; int& ri = *p; // ok, but! 引用一个指针对象是危险的, 因为这个指针对象可能为空, 所以注意不要这么做
      // int *p = nullptr; int& *ri = p; // oops! 引用本身不是一个对象, 所以不能定义指向引用的指针
      int* p = nullptr; int* &rp = p; // ok 指针本身是一个对象, 所以能够定义指向指针的引用

关键字const多才多艺, 尽可能的使用const

const修饰的变量意味着带有”常量”的性质, 该变量不可被修改.

此外

  • 修饰函数的返回值, 令函数返回一个常量值, 避免对函数的结果进行了赋值操作, 也可以避免在比较时将”==”写成”=”而误以为是一个赋值操作, 从而判断总是为真, 造成难以排查的bug.
  • 修饰静态成员变量, 类内初始化了的const static成员变量也仅仅是声明式, 编译器需要坚持在cpp文件中实现一个定义式.
  • 修饰成员函数, 成员函数中不可对成员变量做修改操作, 但有个例外, 用mutable修饰的成员变量, 在即使被const修饰的成员函数中仍能够做修改, 这一点在使用多线程mutex是有所体现.
  • 修饰指针, 指针可以有顶层const和底层const, 顶层const意味着指针本身是一个常量, 底层const意味着指针指向的对象是一个常量. 当执行拷贝时, 顶层const不受影响, 而被拷贝对象有底层const时, 被赋值对象也需要有底层const.
1
2
3
4
5
6
int i = 0;
int* const pi = &i; // const为顶层const, 指针本身不可改变, 但可以通过该指针修改之战指向的对象
const int* cpi = &i; // const为底层const, 指针本身可以改变, 但不可通过该指针修改指针指向的对象
const int* const cpic = &i; // 左边的const是底层const, 右边的const是顶层const
// int* const pic = cpic; // oops! pic需要有底层const, 否则可以通过*pic修改原对象
const int* cpi = cpic; // ok, 有无顶层const没关系, 改变cpi原对象并不会发生改变

默认情况下, const对象只在当前文件内有效, 所以在多个文件之间共享const对象, 需要在变量的定义之前添加extern关键字.

常量表达式

常量表达式, 是指值不会改变且在编译过程中就能够得到计算结果的表达式.

  • constexpr变量, 声明为constexpr类型将由编译器来检查变量的值是否为常量表达式, 声明为constexpr的变量一定是一个常量, 而且必须用常量表达式初始化.
  • 在constexpt声明中定义一个指针, 限定符constexpr仅对指针有效, 与指针指向的对象无关
1
2
const int* p = nullptr; // const为一个底层const, p是一个指向整形常量的指针  
constexpr int* q = nullptr; // q是一个指向整数的'常量指针', constexpr将它所定义的对象置为了顶层const.
  • constexpr函数, 函数的返回类型及所有形参的类型都得是字面值类型, 且函数体必须有且只有一条return语句. 执行初始化任务时, 编译器把对constexpr函数的调用替换成其结果值, 为了在编译过程中随时展开, constexpr函数被隐式的指定为内联函数.

指针 常量和类型别名的坑

1
2
3
4
using pstring =  char*;
// 注意含有指针的类型别名并不是简单的类型替换
const pstring cstr = 0; // 此处修饰cstr的const是一个顶层const, const pstring是指向char的常量指针
类型推导autodecltype
  • 使用auto在一条语句中声明多个变量时, 它们的基础数据类型应当一致.
  • auto一般会忽略掉顶层const, (若推导的结果为指针, 则转换该顶层const保留为底层const), 所以当希望推导结果含有顶层const时, 就需要明确指出顶层const.
  • 对于引用, auto则会保留顶层const.
  • 对简单的代码使用auto可以, 复杂打代码使用auto反而更加复杂, 降低了代码的可读性, 所以不要为了方便而随意的使用auto.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int i = 0, &r = i;
auto a = r; // a的类型为int

const int ci = 1, &cr = ci;
auto b = ci; // b的类型为int, 顶层const被忽略
auto c = cr; // 同b
auto d = &i; // d的类型为指向int的指针
auto e = &ci; // 初始值ci为含有顶层const的指向int类型的常量, 在将其转推导为指针时, 转换该顶层const, 转换为底层const, 所以e的类型为const int*

const auto f = ci; // 需要时明确指定出顶层const

auto &g = ci; // 整数常量引用, 此时顶层const保留
// auto &h = 42; // oops! 引用到一个临时变量
const auto &j = 42; // 常引用可以使用字面量进行复制
auto k = ci, &l = i; // ok, k可以忽略来自ci的顶层const, 所以k和l的基础类型都是int
auto &m = ci, *p = &ci; // ok, 引用保留顶层const, 指针将顶层const转换为底层const, 所以m和p的基础类型一致都为const int
// auto &n = i, *p2 = &ci; // oops! n为int类型的引用, 而p2是含有底层const的int类型的指针
  • 关于decltype花里胡哨的操作, 举些例子看看就好
1
2
3
4
5
6
7
8
9
10
11
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型为const int
decltype(cj) y = x; // y的类型为const int&
// decltype(cj) z; // z的类型为const int&, 所以需要初始化

int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // r + 0的结果类型为int, 所以b的类型为int
// decltype(*p) c; // c的类型为int&, 需要初始化

// decltype((i)) d; // 用()括起来的类型都表示将用引用, d的类型为int&
decltype(i) e; // e的类型为int

左值与右值

判断左值和右值, 看能否取它的地址,能取地址的就是左值, 反之为右值
左值可看作是”对象”,右值可看作是”值”.
左值到右值的转换可看做”读出对象的值”.
std::move允许把任何表达式以”值”的方式处理.
std::forward允许在处理的同时, 保留表达式为”对象”还是”值”的特性.
https://josephmansfield.uk/articles/lvalue-rvalue-metaphor.html
(留坑)

显式类型转换

  • static_cast能处理顶层const的具有明确意义的转换.
  • const_cast则专门处理底层const, 使其恢复”变量”的身份, 一般在函数重载中使用.
  • dynamic_cast在继承及多态时使用.
  • reinterpret_cast从底层的角度来重新解释变量.

这些cast都尽量少用, 尤其是reinterpret_cast, 除非你真的知道它在你的机器上做了什么.

switch内部的变量定义

在case分支定义并初始化变量, 需要将变量定义在块内, 确保后面的所有case标签都在变量的作用域之外.

1
2
3
4
5
6
7
8
9
10
11
switch (num) {
case 1:
// int i = 1; // error C2360: initialization of 'i' is skipped by 'case' label
break;
case 2:
{
int i = 1; // ok
break;
}
default: break;
}

接口与实现分离, 降低编译依赖

  • 头文件中应该以仅有声明式的形式存在, 即向前的类型声明, 对于这种不完全的类型: 可以定义指向这种类型的指针或引用, 也可以作为函数的参数或返回类型. 从而将对象的实现细节隐藏, 降低编译依赖.
  • 使用Interface class解除接口和实现之间的耦合关系,从而降低文件间的编译依赖.

lambda

lambda表达式标准形式: capture lsit -> return type {function body}

  • 可以省略参数列表和返回值类型, 但捕获列表和函数体是必须的, 另外lambda必须使用尾置声明.
  • lambda只有在其捕获列表中捕获一个它所在函数中的局部变量, 才能在函数体中使用该变量.
  • 需要注意的是, 捕获发生在lambda函数创建时, 而不是调用lambda函数的时候
  • 值捕获和引用捕获, 隐式捕获
  • 默认情况下, lambda没有指定返回值时, 函数体中只有一个return是, 编译器能够推断出返回值类型, 而当有多个return时则返回值类型被推断为void, 所以, 当有多个return语句返回时, 需要显示的指定返回值类型.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void func(){
auto lambda_func = [] { return 10; }; // 没有指定参数和返回值

int i = 10;
auto lambda_val_capture_func = [i] { return i; }; // 值捕获
auto lambda_ref_capture_func = [&i] { --i; return i; }; // 引用捕获, 该函数没调用一次 i -= 1

int j = 20;
auto lambda_imp_val_capture_func = [=] { return i + j; }; // 隐式按值捕获用到的变量
auto lambda_imp_ref_capture_func = [&] { auto sum = i + j; i = sum - i; j = sum - i; }; // 隐式按引用捕获用到的变量

// 混合捕获, []内必为'='或'&'开头, 后跟捕获列表
auto lambda_imp_mix_capture_func = [=, &j]{ j = i; }; // 混合捕获, 此时按引用捕获j = 20, 按值捕获i = 10
auto lambda_imp_mix_capture_func2 = [&, j]{ i = j; }; // 混合捕获, 此时按值捕获j = 20, 按引用不过i = 10

lambda_imp_mix_capture_func(); // 结果j = 10
lambda_imp_mix_capture_func2(); // 结果i = 20

auto lambda_return_func = [=]()-> int {
if(i > j) return i; else return j;
};

// 完整的形态
auto lambda_full_func = [=](int a, int b) -> std::vector<int> {
std::vector<int> res = {i,j,a,b};
return res;
};
lambda_full_func(2,3);
}

公有 私有和受保护的继承

  • public继承意味着是is-a关系, 适用于base class上的情况也一定适用于derived class上, 因为每个derived class也是一个base class. 应当仔细斟酌这些classes相互关系之间的差异.
  • private继承而来的所有成员在derived class中都会变为private属性, 无论在base class中原本是protected还是public属性. private继承意味着is-implemented-in-terms of(根据某物实现出), 它通常比组合的级别低, 但当derived class需要访问protected base class的成员, 或需要重新定义继承而来的virtual函数时, 这么设计是合理的.
  • protected继承意味着继承而来的基类成员都将变成protected的, 这时, dervied class中的成员和友元能够访问这些继承而来的base class成员.

虚函数

  • public继承由两部分组成: 函数的接口继承和函数的实现继承.
  • 成员函数的接口总是被继承.
  • 声明为pure virtual函数的目的是为了derived class只继承其接口.
  • 声明为impure virtual函数的目的是为了derived class继承其接口和缺省实现.
  • 声明为non-virtual函数的目的是为了derived class继承其接口和一份强制实现.
  • 多重继承的构造和析构顺序问题

虚继承

虚继承令某个类做出声明, 承诺愿意共享它的基类, 其中共享的基类子对象称为虚基类, 在这种机制下, 无论虚基类在继承体系中出现了多少次, 在派生类中都值包含唯一一个共享的虚基类子对象.

new/delete(留坑)

enum枚举类型

限定作用域的枚举类型: enum class enum_scoped{ … }; // 限定作用域后, 枚举成员的名字在作用域外是不可访问的.
不限定作用域的枚举类型: enum enum_unscoped{ … }; // 不限定作用域, 枚举成员的作用域与枚举类型本身所在的作用域相同.
未命名不限定作用域的枚举类型: enum { … }; // 未命名需要在定义enum时定义枚举的对象.

1
2
3
4
5
6
7
enum color{r, g, b};
// enum color2{r, g, b}; // oops! 重复定义了枚举成员
enum class color3{r, g, b}; // ok.

// 因为作用域的隔离, 所以使用scoped enum成员时需要带上作用域
color3 c3r = color3::r;
color cr = r; // ok.

union 位域

union是一种特殊的类, 一个union可以有多个数据成员, 但在任意时刻只有一个数据成员可以有值.

  • 分配给一个union对象的存储空间至少要容纳它的最大的数据成员.
  • union可以定义包括构造函数和析构函数在内的成员函数, 但是由于union既不能继承自其他类, 也不能作为基类使用, 所以union中不能含有虚函数.
  • 当使用union时, 必须清楚的知道当前存储在union中的值到底是什么类型, 如果是使用了错误的数据成员或者为错误的数据成员赋值, 程序可能会崩溃.
  • 位域的类型必须是整型或者枚举类型.

函数指针 成员函数指针 可调用对象p745 (留坑)

函数指针指向的是函数(某种特定类型)而不是对象, 函数的类型由它的返回值类型和形参类型共同决定, 而与函数名无关.

1
2
3
bool lengthCompare(const int& l, const int& r) {...}
bool(*pf)(const int&l, const int&r); // pf为指向函数的指针
pf = lengthCompare; // 为函数指针赋值

动态内存, 智能指针(留坑)

拷贝控制, 移动对象(留坑)