C++ 程序设计语言

好记性不如烂笔头。

第六章

  • 标准库给出了静态断言,形式类似如下:
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计数变量的名字。原因如下:

  1. 把类型信息加到名字中降低了程序的抽象水平,尤其不利于泛型编程。
  2. 编译器比程序员更擅长记录和追踪类型信息。
  3. 一旦改变某个名字的类型,所有都要改。
  4. 随着用到的类型越来越多,设计的缩写集越来越大,有时含糊不清,有时过于啰嗦。
  • 初始化

  初始化的形式大概有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;
}

  成功的容错系统都是多层级的。按照自底而上的顺序,每一层级在它力所能及的范围内处理更多的错误,把剩下的错误留给更高层级处理。

原文地址:https://www.cnblogs.com/gardenofhu/p/7507886.html