SomeCpp

Last Updated on 2024年2月17日

零碎知识点

  • fflush仅仅是为了输出而设计的, 标准中并没有说明它对输入缓冲的效果.
  • 一元运算符和=是右结合的,这和<<是完全不同的,a=b=c意味着a=(b=c),b=c将先执行,而a<<b<<c则是(a<<b)<<c,a<<b将先执行.
  • C++11后引入了thread_local型的生命周期,这种对象和线程的生命周期是一致的
  • C++11中很多类型都支持列表初始化,a = {value1,value2...}
    • 列表初始化会额外检测是否可能存在信息丢失,如果可能存在丢失,则无法通过编译期检查.
    • 对于自定义类,支持列表初始化意味着存在这样的ClassName(initializer_list<T> lst)构造函数
    • 只要对象支持使用{}构造/赋值,相应的场合都可以使用{},例如return {1,2,3}
  • 使用默认初始化时一定不要加括号,ClassName var()并不是在进行默认初始化,而是一个函数声明.
  • char在参与计算时,具体是有符号/无符号是由编译器决定的.
  • 从C语言继承而来的标准库<name.h>都有其<cname>版本,目前并没有发现使用<cname>的特殊优势
    • #include <cname>后,std::空间内一定有对应的函数名,全局空间可能也有
    • #include <name.h>后,全局空间内一定有对应的函数名,std::空间可能也有
  • 优先使用enum class
    • enum class定义的枚举是强类型的,只能用Color c=Color::red这种表达式,enum则是弱类型的,Light c=0;int d=Light::red这种表达式都是合法的
    • 另外,enum应当出现在头文件中,而不是源代码中
  • 使用usigned int来表示大正数是有瑕疵的,因为代码中往往充斥着类型转换.usigned int类型更适合管理纯粹的二进制数据,这些数据的值意义更加淡化
    • 如果你保持总是使用有符号数,那么就永远不会碰到符号转换引起的错误,可惜的是,size_t这样的历史错误已经无处不在了.
  • 数值字面值对应的默认类型是依赖于编译器的,一定要注意
    • 例如在VS中,字面值常量默认是int型的 long long a = 2^40,看起来没有问题,其实等价为int tmp=2^40; long long a = tmp, 这将导致仅有低32位被保留了
  • C++11起,提供了一个std::to_string()std::stoi()来实现数值和字符串之间的转换.
  • 在C++11中:
    • 整数除法规定为截断,即向0取整,总是删除小数部分.早期版本中则由编译器决定取整的方式.
    • 整数取模m%n的结果,其结果的绝对值为abs(m)%abs(n),符号和m相同
  • 函数形参的默认值可以是"变量",例如void bar(int x=a);这一特性十分危险,一定要慎重使用.
    • 函数形参的默认值最好保持为常量.
  • 所有控制流记号后面都必须跟语句,如果什么也不做,就用;空语句
  • switch后的{}内是一个作用域,case的作用和goto的label相同,并不分割作用域
  • (condition?exprT:exprF)条件表达式仅当exprTexprF都是左值表达式时才返回左值.
  • (expr1,expr2)逗号表达式顺序执行,返回最后一个表达式,这里就是expr2
  • sizeof(EXPR)是一个constexpr,完全在编译期求值.
  • 优先级和结合律并不能处理所有的计算顺序问题,这主要是为编译器留下一定的优化空间.
    • 例如(expr1) opt (expr2),我们可以确定expr1expr2计算完之后才开始opt,但是expr1,expr2具体是哪个先执行,是由编译器决定的.
    • 典型例子: cout<<i<<++i<<endl可以化为cout<<i<<(++i)<<endl,再从左向右结合,得((cout<<i)<<(++i))<<endl, 这里(cout<<i)<<(++i)(++i)(cout<<i)的求值顺序就是不确定的.
  • C语言中,只使用static inline. 这样可以保持和C++一样的语义和用法.(https://stackoverflow.com/questions/216510/extern-inline)
  • 长度为0的"匿名位域"用于声明:占满存储单元的剩余空间 ,从而使得之后的位域从新的存储单元开始.
  • 如果一个struct X的内存布局中最后一个类型是数组,那么我们可以通过malloc(sizeof(struct X) + sizeof(int) *(N - 1))的方式为实例额外分配更大的空间,使得尾部的数组可以"合法"的越界,形成一定的"动态size"的感觉.struct hack的方案比指针作为成员要简单一些,当指针作为成员时,你不可避免的需要做两次malloc,以及对应的两次free
struct Array{
  int N;
  int v[1]
};
  • C++ Parser issue, 下面的两行代码都将被编译器视为函数声明, my_threadmy_thread2都是函数名
    • background_task()相当于T(), 将被parse为函数指针类型,相当于FnT = background_task(); .
    • std::thread my_thread(FnT)进一步被parse为声明了一个函数
    • 一般来说,当我们创建变量时,如果涉及了(), 一定要仔细想想是否正确
class background_task{
public:
  void operator()(){
  }
};

int main() {
  std::thread my_thread(background_task());
  std::thread my_thread2();
}

volatile

  • volatile用于指示编译器"变量可能会被编译器不可见的方式修改", 例如,其地址会被其他库拿到,然后修改.
    • 指针也可以声明为volatile T * p的,用于表明指向的地址可能会被编译器不可见的方式修改.
  • volatile一般意味着:
    • 编译器生成指令时,会尝试绕开所有级别的缓存(L0/L1/L2等), 例如, 每当需要使用值时, 都插入一条新的load, 而不是复用已经在寄存器里的值.
    • 上面的约束也就导致部分编译期优化可能会被禁用.
  • volatile的行为随机器而改变,其用法和const相同,但更加严格,不允许T转换到volatile T也不允许T绑定到volatile T&&

库开发中在.h中暴露API的合理方式:

  • 核心思想: 暴露出的structclass都必须保证不依赖库内对象的内存结构,只能有symbol级别的依赖,不能有地址偏移的依赖.
  • 提供两组接口,xx.h仅暴露C风格接口,xx.hpp仅暴露C++风格的接口,前者一般是后者的一个wrap.
  • 头文件中只能使用Opaque类型,也就是说,除了内置类型,只能使用MyStruct *MyClass *
  • 暴露出的类一定是一个普通类,不能带有虚函数表(或者需要保证虚函数表的结构总是不变).
    • 用户编译时产生的代码如果使用了虚函数,那么这次编译就会依赖库中的虚函数表.库开发者对虚函数表的任意改动都会引入兼容性问题.
  • 暴露出的接口中,不能包含任何编译器自动生成的内容,主要是合成构造函数/合成析构函数/合成拷贝控制.
    • 这些函数在实现时必须是类外定义.
    • 因为客户的编译器合成出的版本极有可能和你的编译器合成的版本不一致.