0
点赞
收藏
分享

微信扫一扫

通用为本,专用为末

一脸伟人痣 2022-01-06 阅读 41
c++
更倾向于通用而不是特殊化

继承构造函数

子类自动获取父类的成员变量和接口

接口是指: 虚函数和纯虚函数

言外之意是说,父类的非虚函数不能被子类继承

构造函数也遵循这个规则 如果父类的构造函数不是虚函数,则不会被子类继承

子类在初始化过程中,要先初始化父类,

        如果父类的构造函数不是虚函数,

        那么子类就不能继承父类构造函数,

        从而不能为父类初始化,

一般子类在自己的构造函数中显示的声明使用父类的构造函数, 这样就解决了父类构造函数不是虚函数也能初始化父类这个问题

struct A {
    A(int i){} //构造函数没有virtual 不是虚函数 不能被子类继承
};

struct B : public A {

    B(int i) : A(i) {} //显示的声明使用父类的构造方法 解决不能继承父类的构造方法
};

问题: 如果基类有很多构造方法 而且这些方法都不是虚函数 那么子类为了使用父类的构造方法,就必须为每一个父类的构造显示的写出一个子类对应的构造函数,这样程序员就累死了

struct A{
    A(int i){}
    A(double d, int i){}
    A(float f, int i, const char * c) {}
    //... 后面还有 假设有10万个
};


struct B : public A {
    B(int i):A(i){}
    B(double d, int i):A(d, i){}
    B(float f, int i, const char * c) : A(f, i, c) {}
    //... 后面还有 假设有10万个 写B类的程序员累死了

    virtual void ExtraInterface{}
};

B的目的是 就为了写一个扩展的接口 或功能 ExtraInterface(){}  但B要初始化父类 那必须显示的写完10万多个父类的构造函数 对应的版本  此时写B类的程序员立马就放弃了c++

为了解决这个问题?c++使用using来声明继承基类的构造函数

struct A{
    A(int i){}
    A(double d, int i){}
    A(float f, int i, const char * c) {}
    //... 后面还有 假设有10万个
};


struct B : public A {
    using A::A; //继承构造函数
    //... 后面还有 假设有10万个 写B类的程序员很轻松

    virtual void ExtraInterface{}
};

问题: 继承构造函数只会初始化父类的成员 子类自己的成员不能被初始化,怎么办?

那么结合使用就地初始化给子类成员一个默认值

struct A{
    A(int i){}
    A(double d, int i){}
    A(float f, int i, const char * c) {}
};


struct B : public A {
    using A::A; //继承构造函数

    virtual void ExtraInterface{}
    
    int b{10}; //就地初始化一个默认值 这样子类成员也能初始化
};

问题: 如果子类需要外部传参数来初始化子类成员变量 那么程序员只能自己来实现构造方法 结合使用继承构造函数 和 初始化列表 当然就地初始化也可以存在 但是列表初始化的优先级高 所以拿的是初始化列表的参数

struct A{
    A(int i){}
    A(double d, int i){}
    A(float f, int i, const char * c) {}
};


struct B : public A {
    B(int i, int j)):A(i),b(j){}
    B(double b, inti, int j):A(d, i),b(j){}
    B(float f, int i, const char * c, int j):A(f,i,c),b(j){}

    virtual void ExtraInterface{}
    
    int b{10};
};

问题: 有时候父类构造函数的参数又默认值。对于继承构造函数来将,参数的默认值是不会被继承的。默认值会导致父类产生多个构造函数的版本,这些多个构造函数版本都会被派生类继承

struct A {
    A (int a = 3, double = 2.4){} 
};

struct B : public A{
    using A:A;
};


//对于A会产生多个构造函数
struct A {
    A();                        //不使用参数的我情况
    A(int a, double = 2.4);     //使用一个参数的情况
    A(int a, double b);         //使用两个参数的情况
    A(const A &);               //默认拷贝构造函数
};

//相应的B继承了A 那么里面会有多个继承构造函数
struct B : public A{
    B():A();                            //不使用参数的我情况
    B(int a, double = 2.4):B(a, 2.4);   //不使用参数的我情况
    B(int a, double b):A(a,b);          //使用两个参数的情况           
    B(const B &rhs):A(rhs);             //默认拷贝构造函数
};

参数默认值会导致多个构造函数版本,因此程序员要特别的小心

问题: 多继承时 子类拥有多个父类 那么多个父类中的构造函数会导致函数名、参数都相同,会产生冲突 二义性

struct A {
    A(int a){}
};

struct B {
    B(int b){}
};


struct C:A,B {
    using A:A;
    using B:B;
};

//解决办法显示的声明一下C的构造函数
struct C:A,B {
    using A:A;
    using B:B;
    C(int){}
};

问题:

        如果父类的构造函数被声明为private成员函数 或者 子类是从父类中虚继承, 那么就不能够在子类中声明继承构造函数   

        如果子类使用了继承构造函数, 编译器不会再为子类生成默认构造函数,那么程序员需要手动写一个无参数的构造函数

#include <iostream>

struct A {
  A(int){}
};

struct B:public A{
  using A::A;
};

int main(int argc,  char* argv[], char **env) {
  B b; //调用隐式删除的“B”默认构造函数
  return EXIT_SUCCESS;
}

委托构造函数

目的是减少程序员书写构造函数的时间

通过委托其他构造函数, 多个构造函数的类 编写起来减少很多代码

class Info{
 public:
  Info():type(1),name('a') {InitRest();}
  Info(int i):type(i),name('a') {InitRest();}
  Info(char e):type(1),name(e) {InitRest();}

 private:
    void InitRest(){}
    int type;
    char name;
};

//发现每个构造函数都使用初始化列表来初始化type和name,并且都调用了相同的函数InitRest() 
//除了初始化列表有的不同 其他的部分都相同, 3个构造函数基本上是相似的 代码存在重复的地方


//改造1 使用就地初始化 确实简单了不少 但是每个都还是调用了InitRest()
class Info{
 public:
  Info() {InitRest();}
  Info(int i):type(i) {InitRest();}
  Info(char e):name(e) {InitRest();}

 private:
    void InitRest(){}
    int type{1};
    char name{'a'};
};

//再次改造  编译器不允许this->Info()不允许在构造函数中调用构造函数
class Info{
 public:
  Info() {InitRest();}
  Info(int i) {this->Info(); type=i;} //编译器报错
  Info(char e){this->Info(); name = 2} //编译器报错 
 
 private:
    void InitRest(){}
    int type{1};
    char name{'a'};
};


//再次改造  黑客技术 虽然绕开了编译器的检查 看起来不错 但是这是在已经初始化一部分的对象上再次调用构造函数, 这就不叫初始化了
class Info{
 public:
  Info() {InitRest();}
  Info(int i) {new (this) Info(); type = i;}
  Info(char e){new (this) Info(); name = 2;} 
 
 private:
    void InitRest(){}
    int type{1};
    char name{'a'};
};


//再次改造  使用委托构造函数
//委托构造函数只能在函数体内为type,name等成员赋初始值,因为委托构造函数不能有初始化列表(不能同时“委派”和初始化列表)
//委托和初始化列表不能同时使用(因为初始化列表优先级高)
class Info{
 public:
  Info() {InitRest();}           //目标构造函数
  Info(int i):Info(){type=i;}    //委托构造函数
  Info(char e):Info(){name = 2;} //委托构造函数
 
 private:
    void InitRest(){type+=1;}
    int type{1};
    char name{'a'};
};
//f(3) 先是4后来又被赋值为3了 目标构造函数比委托构造函数先执行


//再次改造  使用委托构造函数
class Info{
 public:
  Info():Info(1,'a'){}        //委托构造函数
  Info(int i):Info(i,'a'){}   //委托构造函数
  Info(char e):Info(1,e){}    //委托构造函数
 
 private:
    Info(int i, char e):type(i),name(e){type+=1;} //定义一个私有的目标构造函数 替代 InitRest()
    int type;
    char name;
};
//f(3) 就是4


链状的委托构造 不要形成委托环

//正常的链状的委托构造
class Info{
 public:
  Info():Info(1){}            //委托构造函数
  Info(int i):Info(i,'a'){}   //委托构造函数
  Info(char e):Info(1,e){}    //委托构造函数
 
 private:
    Info(int i, char e):type(i),name(e){} //定义一个私有的目标构造函数 替代 InitRest()
    int type;
    char name;
};
//Info()委托Info(int)委托Info(int,char)


//委托环
struct Rule2 {
    int i, c;
    Rule2():Rule2(2){}
    Rule2(int i):Rule2('c'){}
    Rule2(char c):Rule2(2){}
};
//Rule2()委托Rule2(int)委托Rule2(char)委托Rule2(int)
//此时编译器就没办法了 一直在这死循环

委托构造函数应用在构造模版函数产生目标构造函数

class TDConstructed{
  template<class T> TDConstructed(T first, T last) : l(first, last){} //目标构造函数

  std::list<int> l;

 public:
  TDConstructed(std::vector<short> & v):TDConstructed(v.begin(), v.end()){} //委托构造函数
  TDConstructed(std::deque<int> & d):TDConstructed(d.begin(), d.end()){} //委托构造函数
};

//T会分别推导为vector<short>::iterator 和 deque<int>::iterator两种类型
//这样的好处就是 很容易接收多种容器 对其进行初始化 
//总比你罗列不同的类型的构造函数方便的多
//委托构造使得构造函数泛型编程也称为可能

委托构造函数在异常中的使用

#include <iostream>
#include <list>

class DCExcept {
 public:
    DCExcept(double d)
      try : DCExcept(1,d){ //委托构造函数 捕获到了异常 不会执行 try 这块代码
        std::cout << "Run the body." << std::endl;
      }catch (...){
        std::cout << "caught exception." << std::endl; //会执行
      }

 private:
   DCExcept(int i, double d){ //目标构造函数 抛出异常
     std::cout << "going to throw!" << std::endl;
     throw 0;
   }
   int type;
   double data;
};

int main(int argc,  char* argv[], char **env) {
  DCExcept a(1.2); //由于构造函数抛出了异常 而在main 函数中 没有捕获 那么直接调用 std::terminate() 结束程序
  return EXIT_SUCCESS;
}
/**
going to throw!
caught exception.
libc++abi: terminating with uncaught exception of type int

Process finished with exit code 134 (interrupted by signal 6: SIGABRT)
*/

委托构造函数try代码满足了不应该执行 而应该执行catch里面的代码 

右值引用: 移动语以和完美转发

指针成员和拷贝构造函数

类成员有指针成员 就必须特别小心拷贝构造函数的编写 否则一不小心会出现内存泄露

浅拷贝:

#include <iostream>

class HasPtrMem{
 public:
  HasPtrMem():d(new int(0)){}
  ~HasPtrMem(){delete d;}
  int * d;
};


int main(int argc,  char* argv[], char **env) {
  HasPtrMem a;
  HasPtrMem b(a);
  std::cout << *a.d << std::endl; //0
  std::cout << *b.d << std::endl; //0
  return EXIT_SUCCESS; //正常析构
}
/*
primer(29288,0x1116cee00) malloc: *** error for object 0x7fdbe9405b50: pointer being freed was not allocated
primer(29288,0x1116cee00) malloc: *** set a breakpoint in malloc_error_break to debug
0
0
*/
//堆上的内存会被析构两次 第一次正常 第二次就出现错误

深拷贝: 

#include <iostream>

class HasPtrMem{
 public:
  HasPtrMem():d(new int(0)){}
  HasPtrMem(const HasPtrMem &rhs):d(new int(*rhs.d)){}//拷贝构造函数 从堆中分配内存 并用 *h.d初始化
  ~HasPtrMem(){delete d;}
  int * d;
};


int main(int argc,  char* argv[], char **env) {
  HasPtrMem a;
  HasPtrMem b(a);
  std::cout << *a.d << std::endl; //0
  std::cout << *b.d << std::endl; //0
  return EXIT_SUCCESS; //正常析构
}

移动语义

拷贝构造函数中为指针成员分配新的内存再进行拷贝的做法在c++编程中几乎被视为不可违背的

不过有些时候真的不需要拷贝 而且拷贝大块内存非常的消耗资源 性能不高

#include <iostream>

class HasPtrMem{
 public:
  HasPtrMem():d(new int(0)){
    std::cout << "构造函数: " << ++n_cstr << std::endl;
  }
  HasPtrMem(const HasPtrMem &rhs):d(new int(*rhs.d)){
    std::cout << "拷贝构造函数: " << ++n_cptr << std::endl;
  }//拷贝构造函数 从堆中分配内存 并用 *h.d初始化

  ~HasPtrMem(){
    std::cout << "析构函数: " << ++n_dstr << std::endl;
    delete d;
  }

  int * d;
  static int n_cstr;
  static int n_dstr;
  static int n_cptr;
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;

HasPtrMem GetTemp(){
  return HasPtrMem();
}

int main(int argc,  char* argv[], char **env) {
  HasPtrMem a = GetTemp();
  return EXIT_SUCCESS;
}

/* g++ main.cpp -fno-elide-constructors
构造函数: 1
拷贝构造函数: 1
析构函数: 1
拷贝构造函数: 2
析构函数: 2
析构函数: 3
*/

看图

 为了得到一个HasPtrMem需要2次拷贝 2次析构 中间的临时对象等等这些操作,那么性能很差

对比拷贝和移动的模型:

 增加移动构造函数

#include <iostream>

class HasPtrMem{
 public:
  HasPtrMem():d(new int(0)){
    std::cout << "构造函数: " << ++n_cstr << std::endl;
  }
  HasPtrMem(const HasPtrMem &rhs):d(new int(*rhs.d)){
    std::cout << "拷贝构造函数: " << ++n_cptr << std::endl;
  }//拷贝构造函数 从堆中分配内存 并用 *h.d初始化

  HasPtrMem(HasPtrMem && rhs):d(rhs.d){ //移动构造函数 先将传入的对象的指针初始化当前对象的成语
    rhs.d = nullptr; //然后将传入的对象的指针设为nullptr 不进行拷贝 而是直接将指针以赋值的形式 拿过来
    std::cout << "移动构造函数: " << ++n_mvptr << std::endl;
  }

  ~HasPtrMem(){
    std::cout << "析构函数: " << ++n_dstr << std::endl;
    delete d;
  }

  int * d;
  static int n_cstr;
  static int n_dstr;
  static int n_cptr;
  static int n_mvptr;
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvptr = 0;

HasPtrMem GetTemp(){
  return HasPtrMem();
}

int main(int argc,  char* argv[], char **env) {
  HasPtrMem a = GetTemp();
  return EXIT_SUCCESS;
}
/*
构造函数: 1
移动构造函数: 1
析构函数: 1
移动构造函数: 2
析构函数: 2
析构函数: 3
*/

前后对比 拷贝2次和移动2次 效率完全不一样

问题: 左值引用或者使用指针当函数的传出参数 也能达到同样的效果 但是为什么不用呢?

从性能上将这样做没毛病, 但是从使用的方便性上来说差点

比如:

        Caculate(GetTemp(), SomeOther(Maybe(), Useful(Values,2)));

但是用指针和引用的方法而不返回值的话, 通常需要多些很多语句

比如:

        string *a; vector b; //事先声明一些变量

        ...

        Useful(Values,2,a);  //最后一个参数是指针,用于返回结果

        SomeOther(Maybe(), a, b); //最后一个参数是引用,用于返回结果

        Caculate(GetTemp(), b);

最起码你要先定义一个指针,然后传给函数

但是移动就不需要这样,直截了当 在函数return 里面返回临时量就可以了

总之: 程序员就舒服了 使用最简单的语句 完成大量的工作 代码也好看

左值、右值与右值引用

问题: 判断左值和右值

c语言中 一个经典的方法就是

        在赋值表达式中,出现在等号左边的就是左值 不能在左边的就是右值

        比如 a = b+c;  a是左值   b+c = a; 编译器报错 那么b+c就是右值

        这种方法在c++中有时候好用 有时候不好用

c++一种方法是:

        可以取地址的、有名字的就是左值

        不能取地址的、没有名字的就是右值

        比如:  a = b+c; 

                 &a编译器不报错 那么a是左值

                 &(b+c)编译器报错 那么b+c是右值

细致的看 c++11 右值由两个概念构成的 将亡值 和 纯右值

        将亡值:

                 返回右值引用T&&的 函数返回值

                  std::move的返回值

                  转换为T&&的类型转换函数的返回值

        纯右值: 

                 非引用返回的函数返回的临时变量值就是一个纯右值

                 一些运算表达式 比如:1+3产生的临时变量 也是一个纯右值

                 不跟对象关联的字面量值 比如 2, 'c',  true也是纯右值

                  类型转换函数的返回值 也是纯右值                   

                  lambda表达式 也是纯右值

c++中的左值和右值很难归纳

始终记住无论是左值引用还是右值引用 只要是引用都是对象的简称或者别名 引用是不占内存的 所以引用本身不是对象 引用是说你已经有了一个实实在在的对象,我只是绑定到这个实实在在的对象上 所以引用一开始就需要初始化(也可以这么理解: 没有对象,就没办法绑定,也就没办法给对象起别名,所以引用一开始就需要初始化)  无非就是左值引用绑定到一个有名字的对象 而右值绑定到一个没有名字的对象(匿名对象)  还要记住引用一开始初始化绑定到对象后 以后就不能解除绑定和不能绑定到另外一个对象 除非你另起一个名字 也就是再声明一个引用

移动构造函数如何触发?

如何判断产生了临时对象?

如何将临时对象用于移动构造函数?

是否只有临时变量才可以用于移动构造?

举报

相关推荐

0 条评论