C++ 构造函数漫谈(四)

2019/08/21

这一章谈 C++11 中引入的两种 “语法糖” .使用它们可以使得我们的代码更为简洁优雅。

委托构造函数

在同一个类中,一个构造函数可以调用另一个构造函数,这叫委托构造函数。这是 C++ 11 的新特性。
委托构造函数可以简化在每个构造函数中的重复代码。

class B{
public:
    B():x_(0),y_(0),z_(0){
        //Do something
    }
    B(int x): B(){
        x_ = x;
    }

    B(int x, int y): B(x){
        y_ = y;
    }

    void DoSomething(){}
private:
    int x_;
    int y_;
    int z_;
};

注意一点,委托构造函数在使用时不可以形成环:禁止套娃。

继续阅读

Note 4,778

C++ 构造函数漫谈(三)

2019/08/19

这一章聊一聊在面向对象的C++中,构造函数的调用顺序。

数据成员的构造顺序

一个类的数据成员的初始化顺序只与其在类中的声明顺序相关,与其它无关。

而析构时,如果成员是在堆中,析构顺序正好与构造时相反。

class M {
 public:
  M(const char* msg) {
    cout << "M " << msg << endl;
    msg_ = new char[strlen(msg)+1];
    msg_[strlen(msg)] = '\0';
    strcpy(msg_, msg);
  }
  ~M() {
    cout << "~M " << msg_ << endl;
    delete msg_;
  }

 private:
  char* msg_;
};

class A {
 public:
  A() : pm2_(new M("pm2")), m2_("m2"), pm1_(new M("pm1")), m1_("m1") {}

 private:
  M m1_;
  M m2_;
  M* pm1_;
  M* pm2_;
};

int main() {
  A a;
  return 0;
}

类A的成员的构造顺序为: m1_, m2_, pm1_, pm2_ 。析构时的顺序为 m2_, m1_ ,由于 pm1_, pm2_ 不在堆中,所以它们的析构需要类A自己管理。

继续阅读

Note 4,592

C++ 构造函数漫谈(二)

2019/08/18

三五零法则

我们知道,编译器会为类自动生成几个特别的成员函数:构造函数、复制构造函数、复制赋值运算符、析构函数。后三者比较特殊,我们在下面会频繁提到。

三法则

若一个类需要用户显式定义 析构函数、复制构造函数、复制赋值运算符 中的一个,那么这三个函数都需要显式定义 。如果用户显式定义了其中一个,另外两个还是会被编译器隐式定义,这种混杂的情况容易生产无法预期的错误。
如果一个类中有非基本数据类型或者非类类型的成员(如指针、文件描述符等),则这一法则表现的更为明显:隐式析构函数无法对这种成员进行有效的释放,隐式复制构造函数和隐式复制赋值运算符无法进行深拷贝。

继续阅读

Note 4,982

C++ 构造函数漫谈(一)

2019/08/12

C++ 构造函数有很多有意思的小细节。这里来做一些探讨。这些内容可能会分为几章,这一章来探讨 隐式构造函数,显式空构造函数 和 =default 修饰的构造函数 ,私有构造函数和 =delete 修饰的构造函数 之间的区别。

在开始之前,我们先了解两种特殊的类:

聚合类 与 POD

聚合类 是 C++ 中的一个特殊的类型。当一个类(class, struct, union) 满足以下条件时,它是一个聚合类:

  • 无显式声明的构造函数(可以是 defaultdelete 的)
  • 无基类
  • 无虚成员函数
  • 无私有的或受保护的非静态数据成员
  • 无使用 {}= 直接初始化的非静态数据成员

一个普通数组也是一种聚合类型(如 int[10], char[], double[2][3])

POD ( Plain old data structure ) 则是一种特殊的聚合类,它必须满足聚合类的所有条件,且不具有以下成员:

  • 指针到成员类型的非静态数据成员(包括数组)。
  • 非POD类类型的非静态数据成员(包括数组)。
  • 引用类型的(reference type)非静态数据成员。
  • 用户定义的拷贝与赋值算子。
  • 用户定义的析构函数。

可见,POD类类型就是指class、struct、union,且不具有用户定义的构造函数、析构函数、拷贝算子、赋值算子;不具有继承关系,因此没有基类;不具有虚函数,所以就没有虚表;非静态数据成员没有私有或保护属性的、没有引用类型的、没有非POD类类型的(即嵌套类都必须是POD)、没有指针到成员类型的(因为这个类型内含了this指针)

POD 一般用来在不同的模块之前传递数据使用。如一个 C++ 库向外提供 C 接口,可以使用 POD 作为参数。

隐式构造函数,显式空构造函数 和 =default 修饰的构造函数。

对于 未定义任何构造函数 的类型( struct class or union),编译器会为该为自动生成一个 inline public 的构造函数, 如果这个类型满足 constexpr 类型的要求,则这个构造函数还会被 constexpr 修饰,这个由编译器生成的构造函数,我们称之为 隐式构造函数 或 默认构造函数。在 C++11 以前,如果用户声明了其它构造函数,则编译器不会生成默认构造函数,需要我们显式的声明。而在 C++11 以后,我们仍可用 default 关键字来强制编译器自动生成原本隐式声明的默认构造函数。

继续阅读

Note 5,636

C++11: 返回值类型后置语法

2019/06/19

今天在看 cpprestsdk 的源码的时候发现一个方法的定义是这样的:

template<typename _Ty>
__declspec(noinline) auto create_task(_Ty _Param, task_options _TaskOptions = task_options())
    -> task<typename details::_TaskTypeFromParam<_Ty>::_Type>
{...}

语法为:

auto fun_name(argument-declarations...) -> return_type{}
我们知道 C++11 的 lambda 表达式可以使用 `->` 指定返回参数的类型, 没想到声明方法也可以这么用。这种写法和传统的方法有什么区别呢?

在泛型编程中,一个常见的例子如下:

template <typename T>
T add(T a, T b) {
  return a + b;
}

如果 `a` 和 `b` 的类型不同,需要怎么写呢?

template<typename R, typename T1, typename T2>
R add(T1 a, T2 b) {
  return a + b;
}

  auto ret_val = add<float>(1.0, 2);
  auto ret_val1 = add<decltype(1.0 + 2)>(1.0, 2);

我们在调用此方法时必须显示指定 `R` 的类型,  或者使用 `decltype` 运算符推导返回值类型。如果让调用的方式简单点呢?把返回值类型放到定义中去, 像这样:

template <typename T1, typename T2>
decltype(a + b) add(T1 a, T2 b) {
  return a + b;
}

然而这么写是编译不过的。因为 `a`, `b` 在参数列表中,编译器解析返回值的时候,它们还没有定义。那么我们可以这样写:

template <typename T1, typename T2>
decltype((*(T1*)0) + (*(T2*)0)) add(T1 a, T2 b) {
  return a + b;
}

不过这太晦涩了。

C++ 11 允许将返回值类型后置,前面使用 auto 占位:

继续阅读

关于 enable_shared_from_this

2019/05/16

一. 引入

简单地说: enable_shared_from_this 是为了解决 在类的内部获取自己的 shared_ptr 这件事情而存在的。

众所周知, 每一个对象都能通过this 指针来访问自己的地址。this 指针也是所有成员函数的隐含参数。然而有些时候,我们需要的不仅是 this,而是一个 “this的智能指针”。

这里有一个常见的场景:

class A {
public:
    A() :did_it_(false) {}
    ~A() {
        std::cout << "destoried" << std::endl;
    }

    void OnDo(bool did) {
        did_it_ = did;
        std::cout << "somthing did" << std::endl;
    }

    void DoSth_Async() {
        std::thread t([this]() {
            std::this_thread::sleep_for(std::chrono::seconds(5));
            //...do somthing
            OnDo(true);
        });
        t.detach();
    }
private:
    bool did_it_;
};

代码如上:在异步方法 DoSth_Async() 中调用了成员方法 OnDo(bool) . 这里存在一个问题: 当 OnDo() 被调用的时候,该类的是否还在生存中:

int main(){
    {
        std::shared_ptr<A> ptr(new A());
        ptr->DoSth_Async();
    }
    std::this_thread::sleep_for(std::chrono::seconds(5));
    return 0;
}

智能指针 ptr 在出作用域后立即被释放。所以当 OnDo() 被调用的时候,其所在的对象实际已经被释放了。如果确保在 OnDo() 被调用的时候,该对象仍然在生命周期内呢?一个方便的方法便上在构建线程的时候,将该对象的 shared_ptr 传入到线程。在该线程的生命周期内,该对象就会一直存在。这是一种利用 shared_ptr 的 保活机制

继续阅读

从单例模式谈起(一)

2019/04/08

单例模式可能是大家最为熟知的一种设计模式,本身没什么好谈的。但是在 C++ 中,由单例模式可以引出一系列的问题,可能会比较有意思,这里探讨一下。

常见的简单实现

1.使用 static 实现

class S1 {
    public:
        static S1& GetInstance() {
            static S1 s;
            return s;
        }   
        void fun() {}

    private:
        S1(){cout<<"S1 inited"<<endl;}
        S1(const S1&) = delete;
        S1& operator=(const S1&) = delete;
};

class S2{ 
    public:
        static S2& GetInstance(){
            return s;
        }   
        void fun(){}
    private:
        S2(){cout<<"S2 inited"<<endl;}
        //...
    private:
        static S2 s;
};
S2 S2::s;

其中 S2 是要避免的。因为 1. 类的静态成员变量的初始化时间一般早于 main 函数 2. 如果静态成员的初始化里调用了其它类,可能出现未定义的错误。

2.使用指针判断是否初始化

class S3{
    public:
        static S3& GetInstance(){
            if(s == 0)
                s = new S3();
                 return *s;
        }

    private:
        S3(){}
        static S3 *s;
};

这两种方式在单线程程序中使用都是可以的,但如果在多线程中,就会出现问题。

Magic Static

对于 S1 要注意, C++ 局部静态变量的初始化可能不是线程安全的 , 这就是 Magic Static ,是指 返回一个静态局部变量的引用 的用法,在某些情况下,如S1 可能会被编译器这样解析:

继续阅读

TCP 的状态(二) : TCP 常见错误码

2019/04/01

TCP 协议错综复杂, 很容易出现错误,错误码非常之多。这里探究一些在编程中比较常见的错误码。tcp 错误码 在不同的操作系统中的值不同。这里取用 Linux 与 Windows 两种,如 connection_refused 在windows  中的值为10054, 在 Linux 中为54 .

eof :

  • 2 : End of file

eof 标志着流的结束。当对端关闭连接(调用了 shutdownclose) 后,处于 CLOSE_WAIT 状态。 本端会收到 FIN, ACK 。此时如果本端再试图 read ,则会读到 eof

connection_refused

  • 61 : Connection refused
  • 10061 : No connection could be made because the target machine actively refused it . 由于目标计算机积极拒绝,无法连接

在客户端试图与服务端建立连接的时候发生。一般是服务端没有处于监听状态。 客户端发送发 SYN , 但是收到了 RST, ACK

connection_reset

  • 54 : Connection reset by peer
  • 10054 : An existing connection was forcibly closed by the remote host . 远程主机强迫关闭了一个现有的连接。

对端对处于连接状态的socket 进行了异常断开, 如进程中断等。此时如果本端进行读操作,可能会得到此错误。 继续阅读

Note 8,257