好记性不如烂笔头。
第六章
- 标准库给出了静态断言,形式类似如下:
stastic_assert(A,S);//当A不为true时,把S作为一条编译器错误信息输出
其最重要的用途是为泛型编程中作为形参的类型设置断言
- void有两个作用:1,作为函数的返回类型用以说明函数不返回任何实际的值。2,作为指针的基本类型部分以表明指针所指对象的类型未知。
- 在一些机器体系结构中,存放变量的字节必须保持一种良好的对齐方式,以便硬件在访问数据结构资源的时候足够高效。alignof()运算符返回实参表达式的对齐情况,例如:
auto ac = alignof('c');//char的对齐情况
auto al = alignof(1); //int的对齐情况
- 在为实体命名时,应尽量让名字反应实体含义而非实现细节。在C++中,以下形式并不被建议使用,pcname作为某个char*的名字,或者icount作为某个int计数变量的名字。原因如下:
- 把类型信息加到名字中降低了程序的抽象水平,尤其不利于泛型编程。
- 编译器比程序员更擅长记录和追踪类型信息。
- 一旦改变某个名字的类型,所有都要改。
- 随着用到的类型越来越多,设计的缩写集越来越大,有时含糊不清,有时过于啰嗦。
- 初始化:
初始化的形式大概有4种:
X a1{v}; //C++11中提出
X a2 = {v}; //继承自C语言
X a3 = v; //继承自C语言
X a4(v);
以上4种形式的初始化,只有第一种不受限制使用。一般推荐使用{}方式初始化,使用{}初始化称之为列表初始化(list initialization),好处是防止窄化转换。这句话的意思是:如果一种整型(浮点型)不能存放下另一种整型(浮点型)的值,则后者不会被转化为前者。(另外,整型值不能转化为浮点型的值)。因此,没有特别的理由,最好使用{}进行初始化。
- 初始化器缺失时:
若是未指定初始化器,则全局变量,名字空间变量,局部static变量,和static成员会执行相应数据类型的列表{}初始化。对于局部变量,和自由存储上的对象(动态对象),除非它们位于用户自定义类型的默认构造函数中,否则,不会执行默认初始化。若相对内部局部变量或者new创建的内置类型的对象进行初始化,使用{}的形式,例如:
void ff()
{
int x{};
char buf[12]{};
int* p{new int{10}};
char*q {new char[1024]{}};
}
- 推断类型:
c++提供两种从表达式中推断数据类型的机制。1.auto根据对象的初始化器推断对象的数据类型。可能是变量,const或者constwxpt的类型。2.decltype(expr)推断的对象不是一个简单的初始化器,有可能是函数的返回类型或者类成员类型。
- 右值
当考虑对象的寻址,拷贝,移动等操作时,有两种属性很关键:
1.有身份(has identity):在程序中有对象的名称,或指向该对象的指针,或该对象的引用。这样我们就能判断两个对象是否相等,或者对象的值是否发生改变。
2. 可移动(is movable):能把对象的内容移动出来。
在上述2中属性的四种组合形式中,有3种需要C++语言规则精确的描述(没有身份又不能移动的对象不重要)。用m表示可移动,用i表示有身份,分类如下:
左值有身份,但不能移动;右值是允许执行移出操作的对象。
- 类型别名:
typedef void(*PtoF)(int); //等价于 “using PtoF = void(*)int;”
- 指针:
大多数机器支持逐字节访问内存,其他机器则是从字中抽取字节,很少有机器支持直接寻址到二进制位。能独立分配且用内置指针指向的最小单位位char类型的对象。(bool类型占用的内存空间至少与char类型一样)。如果想要把更小的值存的更紧密,可使用位逻辑操作,结构中的位域,或者bitset。
- void* :
指向未知类型的对象的指针。除了函数指针(Why)和指向类成员的指针(why 和 why2)外,指向其他任意类型对象的指针都能赋值给void*类型指针。两个void* 可以比较是否相等,还能显式的把他转化为其他类型。
函数指针转换会带来不可预知的风险。 member functions中则隐含了this指针。具体参考链接中说明。
- 字符串字面常量:
字符串字面常量的类型是“若干个const字符组成的字符数组”,“abcd”的类型是const char[5]。下述代码是不安全的:
char* p = "abcd"; //错误,但C++11之前被接受
p[3] = "d"; //错误,试图为常量赋值
- 原始字符串:
当字符串中出现大量的反斜线和双引号,原始字符串就避免了转义字符中的反斜线的重复出现,避免了书写错误。
string s = R"(\w\\ww\\)"; //s的内容是 \w\\ww\\
string s = R"("strings")"; //s的内容是 “strings”
- 指针与const
一个指针牵涉到两个对象,即指针本身和所指向的对象。在指针声明中,前置const关键字将令所指的对象而非指针本身成为常量。若要指针本身成为常量,则需要用 *const 代替 * 。
char * s = "abcd";
const char * p = s; //指向的内容为常量,p是指向const char的指针
char *const p2 = s; //指针为常量, p2是指向char的const指针
技巧是:从右向左读。
- 右值引用:
非const左值引用的对象可以由用户写入内容。const左值引用的对象从用户角度来看是不可修改的。右值引用对应一个临时对象,用户可以修改这个对象,并且认为这个对象以后不会被用到了。参考链接。
- 返回值
下面两种返回值形式是一样的
string to_string(int a); //前置返回类型
auto to_string(int a) -> string; //后置返回类型
- 局部变量:
定义在函数内部的名字通常称为局部名字,当线程执行到该定义处,它们被初始化。除非我们把变量声明为static,否则函数每次调用都会拥有该变量的一份拷贝。相反,如果我们把局部变量声明为static,则函数的所有调用中将使用唯一的一份静态分配的对象,该对象在线程第一次到达的时候进行初始化。
static局部变量有一重要作用:它可以在函数的多次调用间维护一份公共的信息,而无须使用全局变量。如果使用了全局变量,则有可能会被其他不想关的函数访问甚至干扰。
递归的初始化一个局部static变量会产生未定义的结果。
- 重载函数
重载函数并不考虑返回值,其和作用域有着密切关系。基类和派生类的作用域不同,因此默认情况下基类函数和派生类函数不会发生重载。如果想要实现跨类作用域或者跨命名空间作用域的重载,应该使用using声明或者using指示。依赖于参数的查找也会导致跨名字空间重载。
- 前置与后置条件
函数调用时应遵循的约定称为"前置条件",函数返回时应遵循的条件称为“后置条件”。
- 函数指针
与数据对象类似,由函数体生成的代码也置于某块内存区域中,因此它也有自己的地址。函数指针并不能修改所指向的代码(与机器体系有关,于系统设计有关)。调用者只能对函数指针做两种操作:调用它或者获取它的地址。通过获取地址得到的指针能被用来调用该函数。
void error(string s) {/* ... */}
void (*efct)(string); //指向函数的指针,该函数接受一个字符串参数
void f()
{
efct = &error; //efet 指向error
efct("error"); //通过efct调用error
}
编译器发现efct是函数指针,因此会调用它所指的函数。即,解引用函数指针时可用 * ,也可不用。同样,获取函数地址时可以用&,也可以不用:
void(*f1)(string) = &error; //OK: 等价于 = error
void(*f2)(string) = error; //OK: 等价于 = &error
void g()
{
f1("Vasa"); //OK: 等价于(*f1)("Vasa")
(*f1)("Mary Rose"); //OK: 等价于f1("Mary Rose")
}
函数指针的参数类型声明与函数本身类似,进行指针赋值操作时,要求完整的函数类型都必须精确匹配。
C++允许把一个函数指针类型转换成别的函数指针类型,但之后必须把得到的结果指针转换为其原来的类型,否则会出现意外情况:
using p1 = int(*)(int*);
using p2 = void(*)(void);
void f(p1 pf)
{
p2 pf2 = reinterpret_cast<p2>(pf);
pf2(); //可能会出现严重错误
p1 pf1 = reinterpret_cast<p1>(pf2); //把pf2 转换回来
int x = 7;
int y = pf1(&x);
//...
}
- 异常处理:
函数如果希望处理某个问题,可以捕获相应的异常:
1 主调组件如果想处理某些失败的情形,可以把这些异常置于try块的catch从句中。
2 被调组件如果无法完成既定的任务,可以用throw表达式抛出一个异常来说明这一情况。
void taskmaster()
{
try{
auto result = do_task();
//使用result
}
catch(Some_error)
{
//执行do_task时发生错误:处理该错误
}
}
int do_task()
{
//...
if(/*能够执行该任务*/)
return result;
else
throw Some_error;
}
成功的容错系统都是多层级的。按照自底而上的顺序,每一层级在它力所能及的范围内处理更多的错误,把剩下的错误留给更高层级处理。