2020-08-29-C++编程思想

Record

Posted by Jocelyn on August 29, 2020

2020-08-29-recording.

[TOC]

预处理器-宏-效率

  • 宏可以 不用普通函数的调用代价就可以使之看起来像是函数调用

  • 宏的实现使用 预处理器 而不是 编译器。预处理器会直接使用 宏的编码 而不是 宏的调用,所以没有 参数压栈、生成汇编语言的CALL,返回参数,执行汇编语言的RETURN等开销

  • 缺点:预处理器不允许访问 类的成员数据,所以预处理器 宏不能用作类的成员函数

  • 所以为了 预处理器 宏的效率 还能够像成员函数一样 在类里 访问自如,使用 内联函数

    #define F (x) (x+1)
      
    调用的时候F(1),会出现 不希望的情况: (x)(x+1) (1)
    因为在F 和 括号 之间 存在空格
        
    #define F(x) (x+1)
    这种情况,即使 F (1)这样调用也是可以正常展开的 
        
        
    if(a&0x0f >= 0x07? 0:1)
    涉及到优先级的问题:>= 的优先级 高于 &
    

内联函数

  • 内联函数 无论从哪方面来说 都是一个 真正的函数,内联函数可以在 适当的地方 像 宏一样展开,所以不需要函数调用的开销

  • 任何在类中定义的函数 自动地 成为 内联函数,可以加,但不是必须的

  • 也可以在 非类 的函数前面加上 inline关键字 使之成为 内联函数。为了使之有效,必须将函数函数体和声明结合在一起,否则编译器会将它 作为普通函数对待

    inline int plusOne(int x);
      
    inline int plusOne(int x){ return ++x;}
    
  • 编译器可以进行参数列表是否使用正确进行检查 并返回值,预处理器是无法做到的

  • 一般将 内联函数 放到 头文件中,当编译器看到这个定义时,会将函数类型(函数名+返回值)和函数体 放到 符号表,当使用函数时,编译器会进行检查 确保调用 正确,返回值被正确使用,然后将 函数调用 替换为 函数体,从而消除 开销

  • 在头文件中,内联函数 处于特殊状态,头文件中有声明和定义,但是该头文件被多个文件包含,不会产生 多个定义的情况

  • 内联函数,最重要的 使用之一 作为 访问函数,允许读 或者修改 对象 状态

  • 内联只是 编译器的 一个建议,不会强迫内联任何代码

  • 向前引用

    class Forward(){
      int i;
    public:
      Forward():i(0){}
      int f() const {return g() + 1;}
      int g() const {return i;}
    };
    函数f()调用g(),此时还没有声明g(),也可以正常工作,因为C++规定:只有在类的声明结束后,其中的内联函数才会被计算。
    
  • 类中内联的例子

    class Rectangle{
      int width,height;
    public:
      Rectangel(int w = 0,int h = 0) : width(w),height(h){}
      int getWidth() const;
    }
    inline int Rectangle::getWidth() const
    {
      reutrn width;
    }
    
  • 标志点粘贴

    作用是:先分隔,然后进行强制连接

    #define A1(name,type) type name_##type##_type
    #define A2(name,type) type name##_##type##_type
    A1(a1,int);等价于 int name_int_type
    A2(a1,int);等价于 int a1_int_type
    ##用于分隔,然后看分隔后的内容哪个是被定义过的
    

static and namespace

  • static 在固定地址上进行存储分配,在特殊的 静态数据区,而不是在堆栈上
  • static使得可见性,在当前翻译单元 或者类之外是 不可见的
  • static int test;(等效于static int test = 0;) 如果没有为 内建类型 的静态变量 提供初始值,编译器会确保 在程序开始时 它被初始化为0,但只针对 内建类型有效,对于用户自定义类型,需要用构造函数来初始化

静态对象的析构函数

  • 静态对象 不单单指的是 局部静态对象,比如全局变量等
  • 静态对象的析构 在从main()中退出时,或者 标准的C库函数 exit()被调用时 才被调用;多数情况 main()函数的结尾 调用 exit()来结束程序,所以在 析构函数内 使用 exit()很危险,会是无限递归;如果使用标准的C库函数 abort()来退出程序,静态对象的析构是不会被调用的。标准C库函数 atexit()来指定当程序跳出main()(或者调用exit())的时候应该执行的操作

销毁顺序

  • 同普通对象的销毁一样,静态对象的销毁也是按与初始化的顺序相反。

    class Obj{
      char c;
    public:
      Obj(char cc):c(c){
        cout<<"creating Obj"<<endl;
      }
      ~Obj{
        cout<<"delete Obj"<<endl;
      }
    };
    Obj test;
    void f(){
      static int a;
    }
    void g(){
      static int b;
    }
    int main()
    {
      cout<<"get in the main()"<<endl;
      f();
      g();
      cout<<"leaving main()"<<endl;
    }
    初始化顺序为 f g
    析构顺序为   g f 
    
  • 全局对象在 main()执行之前被创建,在退出main()时 销毁

    输出的顺序为
    test的构造
    get in the main()
    f的构造
    g的构造
    g的析构
    f的析构❌
    leaving main()
    test的析构
      
      
    test的构造
    get in the main()
    f的构造
    g的构造
    leaving main()
    g的析构
    f的析构
    test的析构
    

控制连接

外部连接:

  • 一般情况,在文件作用域,所有名字(不是嵌套在类 或者 函数 中的名字),对于 程序的 所有 翻译单元 都是可见的,这就是所谓的外部连接。

  • 因为 在连接时,这个名字对于 连接器 来说 是可见的,所以对于 单独的 翻译单元 来说,它是外部的。

  • 全局函数 和 普通函数 都有 外部连接
  • 在文件作用域内 static的 对象 或者 函数名 对 翻译单元 来说 是局部于 该单元的。这些 名字 有 内部连接。所以在 其他翻译单元 可以有 相同的名字 而不会 发生冲突。
  • 在头文件中 常量 内联函数 在默认情况下 都是 内部连接的。
  • 连接 只引用 那些在连接/装载期间 有地址的成员,所以 类声明 和 局部变量 是 没有连接的

static/extern

如果是全局变量
int a = 0;
extern int a = 0;效果是一样的
 
如果定义了
static int a = 0;a成为 内部连接,但是存储类型没有改变,对象总是在 静态数据区
  
如果将局部变量声明为 extern
int main()
{
  extern int i;表示已经存在在某存储区
}

namespace

  • namespace之间可以互相嵌套

  • namespace定义的结尾,不必加 分号

  • namespace定义的内容 可以在多个头文件中延续 补充

  • 一个namespace的名字 可以用 另外一个名字 作为 别名,使得简洁

    namespace BobSuperDuperLibrary{
      .....
    }
    namespace Bob = BobSuperDuperLibrary;
    
  • 不能创建 namespace的实例

  • 每个翻译单元 可以包含 一个 未命名的 命名空间

    namespace
    {
      class Arm{...}
      class Leg{...}
    }
    
  • 友元

    namespace Me{
      class Us{
        //...
        friend void you();
      };
    }
    这样函数you()就可以成为 命名空间 Me 的一个成员
    
  • 使用声明

    void test()
    {
      using namespace U;
      using V::f;
      f();使用的是V::f()
      U::f();
    }
    
  • 不要将 一个全局的 using指令 引入到 一个头文件中,因这样 包含这个头文件的 其他头文件 也可以打开 这个 名字空间

静态数据成员

总结为 内置类型+const才可以在 类内定义,数组不是内置类型
class Values{
    static int size = 1;//不可以,因为不是const 所以定义 需要在 类外 实现
    
    static const int scSize = 1;//内置类型+const 可以在 类内定义 也可以 在类外定义
    static const float scFloat;
    
    static const int scInts[];//即使是const 也不能 类内 定义,因为 是数组
    static float scFloats[];
};
const float Values::scFloat = 2;
const int Values::scInts[] = {22,33,44};
float Values::scFloats[] = {222.3,44.5};
可以都放到 头文件中,也可以放到实现中,因为这些定义时内部连接的

嵌套类和局部类

嵌套类可以有 静态成员变量,局部类不可以有静态成员变量
  
嵌套类:
class Outer{
  class Inner{
    static int i;
  }
}

局部类:
void f(){
  class Local{
    static int i;❌因为办法定义 i
  }
}

静态成员函数

  • 静态成员函数不能访问 一般的 数据成员,只能 调用 静态成员函数,只能调用 静态数据成员
  • 静态成员函数 没有 this指针,所以 无法访问 一般的 数据成员

单实例

class Egg{
  static Egg e;//这里构造了一个 实例 静态的
  int i;
  Egg(int ii):i(ii){}
  Egg(const Egg&);//拷贝构造
public:
  static Egg* instance(){retrun &e;}
  int val() const {return i;}
}
Egg Egg::e(47);

如果拷贝构造函数 可以调用下面来创建对象
Egg e = *Egg::instance();
Egg e2(*Egg::instance());

静态初始化的相依性

  • 在一个 指定的翻译单元 中,静态对象的 初始化顺序 严格按照 该对象在 该单元中 定义出现的顺序,清除的顺序 与 初始化的顺序 相反。
  • 但是对于 作用域为 多个翻译单元的情况下,不能严格保证初始化顺序,可能引起问题
//first file
#include <fstream>
std::ofstream out("out.txt");

//second file
#include <fstream>
extern std::ofstream out;

class Oof{
public:
  Oof(){
    std::out<<"ouch";
  }
}oof;

如果在建立 可执行文件的时候,第一个文件先初始化 没有问题
如果第二个文件先初始化,因为out还没有创建,引起问题
  
所以 避免出现 相互依赖的 静态对象

替代连接说明

如果在C++编写程序 需要用到 C 的库:
声明一个 C 函数
float f(int a, char b);
C++编译器 会将名字 变成 _f_int_char之类的东西 来支持函数重载,但是 C 编译器不需要 _f,这样编译器 将无法 解释 C++  f 的调用
  
替代连接说明 通过 重载 extern 关键字实现
extern "C" float f(int a, char b);
这会告诉编译器f()是用C连接的
标准的 连接类型指定符 有”C C++
  
如果有一组替代连接声明,可以将它们放在花括号中
extern "C"{
  float f(int a, char b);
  double d(int a, char b);
}
或在头文件中
extern "C"{
  #include "Myheader.h"
}