C++点点滴滴(中)

Author Avatar
Magicmanoooo 3月 09, 2019
  • 在其它设备中阅读本文章

std::bind

定义在<functional>,其原型为:

template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );

template< class R, class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );

f生成的forwarding call wrapper。调用这个wrapper就如同调用f,其中一些参数绑定到args上。

其中,f是callable object(function object, pointer to function, reference to function, pointer to member function, or pointer to data member),一些参数将会与之进行绑定。args则是需要进行绑定的参数列表。未进行绑定的参数则由占位符_1, _2, _3...代替。

例子:

void f(int n1, int n2, int n3, const int& n4, int n5) {
    std::cout<< n1 << ' '<< n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << std::endl;
}

int g(int n1) { return n1; }

struct Foo {
    void print_sum(int n1, int n2) {
        std::cout<< n1+n2 <<std::endl;
    }
    int data = 10;
};

int main() {
    int n = 7;
    auto f1 = std::bind(f, std::placeholders::_2, std::placeholders::_1, 42, std::cref(n), n);
    n = 10;
    f1(1, 2, 1001);
    /// 2 1 42 10 7

    // 相当于调用f(12, g(12), 12, 4, 5)
    auto f2 = std::bind(f, std::placeholders::_3, std::bind(g, std::placeholders::_3), 
                std::placeholders::_3, 4, 5);
    f2(10, 11, 12);
    /// 12 12 12 4 5

    // bind to a pointer to member function
    Foo foo;
    auto f3 = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1);
    f3(5);
    /// 100

    // bind to a pointer to data member
    auto f4 = std::bind(&Foo::data, std::placeholders::_1);
    std::cout << f4(foo) << std::endl;
    // 10

    // smart pointer可以用来调用members of the referenced objects
    std::cout << f4(std::make_shared<Foo>(foo)) <<'\n'
                << f4(std::make_shared<Foo>(foo)) <<std::endl;
    // 10
    // 10
}    

std::ref or std::cref

定义在<functional>,两者是用于生成std::reference_wrapper的helper function。

bind的基本原理

bind实际上采用的是延迟计算(lazy evalution)的思想,即将可调用对象保存起来,然后在需要的时候再调用

要实现一个bind需要解决两个问题:

  • 保存可调用对象及其形参
  • 如何实现调用

保存可调用对象及其形参

首先要解决的问题是:如何将可调用对象保存起来,以便在后面调用。要保存可调用对象,需要保存两个东西,一个是可调用对象的实例,另一个是可调用对象的形参。

保存可调用对象的实例比较简单,因为在进行bind时,需要直接传入这个可调用对象,所以可以将其作为一个成员变量(member variable)即可。而保存可调用对象的形参相对麻烦一点,因为形参是变参,不能直接将变参作为成员变量。如果要保存变参的话,需要使用tuple

实现调用

bind的形参是变参,可以是0个,也可能是多个(大部分情况下是占位符,还有可能占位符和实参都有)。正是由于bind绑定的灵活性,导致在调用的时候需要找出哪些是占位符,哪些是实参。如果某个一参数是实参便不处理,如果是占位符,就需要将这个占位符替换为对应的实参。

比如我们绑定了一个三元函数:auto f = bind(&func, _1, 2, _2);。调用为:f(1,3);。一共三个参数,一个实参,两个占位符,调用时传入了两个实参,这时就需要将占位符_1替换为实参1,占位符_2替换为实参3占位符的替换需要按照调用实参的顺序来替换,如果调用时的实参个数比占位符要多,则忽略多余的实参
  
调用的实参会先被转换为tuple,以便接下来替换占位符时,可以选取合适的实参。

bind实现的关键技术

tuple展开为变参

在绑定可调用对象时,将可调用对象的形参(可能含占位符)保存起来(保存到tuple中)。到了调用阶段,需要反过来将tuple展开为可变参数,因为这个可变参数才是可调用对象的形参,否则就无法实现调用了。

需要借助于一个整形序列(integer sequence)来将tuple变为可变参数。在展开tuple的过程中,还需要根据占位符来选择合适实参,即占位符要替换为调用实参。

根据占位符来选择合适的实参

此处比较关键,因为tuple中可能含有占位符。在展开tuple时,如果发现某个元素类型为占位符,则从由调用实参生成的tuple中取出一个实参,用来作为变参的一个参数;当某个类型不为占位符时,则直接从绑定时生成的形参tuple中取出参数,用来作为变参的一个参数。最终tuple被展开为一个变参列表,列表中没有占位符,全是实参,就可以实现调用。

【注意】:在替换占位符时,需要通过占位符的模板参数I来从tuple中选择合适的参数,因为占位符place_holder<I>的实例_1实际上place_holder<1>, 占位符实例_2实际上是place_holder<2>,所以可以根据占位符的模板参数来获取其顺序。

具体实现

/// 借助整形序列来将tuple变为可变参数
/// 定义一个整形序列index_sequence
template<int...>
struct index_sequence {};

/// 通过继承的方式,将参数包展开
template<int N, int...Indices>
struct make_index : make_index<N-1, N-1, Indices...> {};

/*
make_index<1,2,3>::type的展开过程:

    make_index<3> : make_index<2, 2>{ }
    make_index<2, 2> : make_index<1, 1, 2>{ }
    make_index<1, 1, 2> : make_index<0, 0, 1, 2> {
        typedef index_sequence<0, 1, 2> type;
    }
*/
template<int...Indices>
struct make_index<0, Indices...> {
    typedef index_sequence<Indices...> type;
};

template<int I>
struct place_holder {};

place_holder<1> _1;    
place_holder<2> _2;    
place_holder<3> _3;    
place_holder<4> _4;    
place_holder<5> _5;    
place_holder<6> _6;    
place_holder<7> _7;    
place_holder<8> _8;    
place_holder<9> _9;    
place_holder<10> _10;        

/// template< class F, class... Args >
/// /*unspecified*/ bind( F&& f, Args&&... args );

/// template< class R, class F, class... Args >
/// /*unspecified*/ bind( F&& f, Args&&... args );

///成员类型result_type
// 1) 若F是指向函数或指向成员函数的指针,则result_type为F。
//    若F是具有嵌套typedef result_type的class type,则result_type为F::result_type 。
//      否则不定义result_type。
// 2) result_type直接为R 。

/// result type traits
template<typename T>
struct result_traits : result_traits<decltype(&T::operator())> {};

template<typename T>
struct result_traits<T*> : result_traits<T> {};

// 1)function pointer  
template<typename Ret, typename...Args>
struct result_traits<Ret(*)(Args...)> { typedef Ret type; };

// 1)member function
template<typename Ret, typename Cls, typename...Args>
struct result_traits<Ret(Cls::*)(Args...)> { typedef Ret type; };

/// 如果类型不是占位符,则直接从tuple中取出(实际上是直接将此类型forwarding)
template<typename T, typename Tuple>
inline auto select(T&& val, Tuple&) -> 
    typename std::add_rvalue_reference<decltype(std::declval<T>())>::type {
        return std::forward<T>(val);
}

/// 如果类型是占位符,则需要根据占位符选择实参(实参一开始就被放入tuple中,直接根据index选取即可)
/// _1实际上为place_holder<1>。根据place_holder的模板参数I,从tuple中选择参数
/// 在tuple中,index从0开始,所以要获取place_holder<I>占位符的参数,则应在tuple中使用std::get<I-1>(tp)
template<int I, typename Tuple>
inline auto select(place_holder<I>&, Tuple& tp) -> 
                decltype(std::get<I-1>(tp)) {
    return std::get<I-1>(tp);
}

// 判断T是否为一个pointer,但不能检测是否为成员指针或者成员函数指针
template<typename T>
struct is_pointer_noref : 
    std::is_pointer<
        typename std::remove_reference<T>::type
    > {};

// 判断T是否为一个non-static member function pointer(且不是*&这种类型)
template<typename T>
struct is_member_function_noref :
    std::is_member_function_pointer<    
        typename std::remove_reference<T>::type
    > {};

/// 根据不同callable object,将调用不同的invoke函数
//callable object:
//  1. function object
//  2. pointer to function
//  3. reference to function
//  4. pointer to member function
//  5. pointer to data member(先不考虑)

/// 2. pointer to function(函数指针)
/// ===> Func是一个指向对象或者函数的pointer
template<typename Ret, typename Func, typename...Args>
inline typename std::enable_if<
    is_pointer_noref<Func>::value, 
    Ret>::type invoke(Func&& f, Args&&...args) {
        return (*std::forward<Func>(f))(std::forward<Args>(args)...);
        //    (*f)(args...)
}

/// pointer to function()
/// ===> Func是一个指向函数或者对象的pointer,
///         第一个实参是一个member function pointer
template<typename Ret, typename Func, typename Arg1, typename...Args>
inline typename std::enable_if<
    is_member_function_noref<Func>::value && 
        is_pointer_noref<Arg1>::value,
    Ret>::type  invoke(Func&& f, Arg1&& ptr, Args...args) {
        return (std::forward<Arg1>(ptr)-> 
            *std::forward<Func>(f))(std::forward<Args>(args)...);
        /// (ptr->*f)(args...)
}     

/// 4. pointer to member function
/// ===> 不是一个指向对象或者函数的pointer,而是一个member function pointer
template<typename Ret, typename Func, typename Arg1, typename...Args>
inline typename std::enable_if<
    is_member_function_noref<Func>::value && 
        !is_pointer_noref<Arg1>::value,
    Ret>::type invoke(Func&& f, Arg1 obj, Args...args) {
        return (std::forward<Arg>(obj).
            *std::forward<Func>(f))(std::forward<Args>(args)...);
        // (obj.*f)(args...)
}

/// 既不是pointer,也不是member function,则只能是
template<typename Ret, typename Func, typename...Args>
inline typename std::enable_if<
    !is_pointer_noref<Func>::value && 
        !is_member_function_noref<Func>::value,
    Ret>::type invoke(Func&& f, Args...args) {
        return std::forward<Func>(f)(std::forward<Args>(args)...);
        // f(args...)
}

template<typename Func, typename...Args>
struct bind_t {
    // 获取callable object的type
    typedef typename std::decay<Func>::type callable_type;
    // 将所有的参数类型都压入tuple中
    typedef std::tuple<typename std::decay<Args>::type...> argument_type;
    typedef typename result_traits<callable_type>::type result_type;
public:
    template<typename T, typename...BindArgs>
    bind_t(Func& f, BindArgs&...args) : m_func(f) , m_args(args...) {}

    // move ctor
    template<typename Func, typename...BindArgs>
    bind_t(Func&& f, BindArgs&&...args) : 
        m_func(std::move(f)) , m_args(std::move(args)...) {}

    /// call:根据传入的每一个实参,去一一对应相应的形参
    // std::tuple_size:获取传入的实参的数量
    // make_index:对传入的实参进行编号
    // forward_as_tuple:将callable object中的形参压入tuple中    
    template<typename...CallableArgs>
    result_type operator()(CallableArgs&&...cal_args) {
        return call(typename make_index<std::tuple_size<
                        argument_type>::value
                    >::type(),
                    std::forward_as_tuple(std::forward<CallableArgs>(cal_args)...));
        // template <typename... Types> 
        // tuple<Types&&...> forward_as_tuple(Types&&... elements) {
        //         return tuple<Types&&...>(std::forward<Types>(elements)...);
        // }
    }

    template<int...Indices, typename ArgumentTuple>
    result_type call(index_sequence<Indices...> in, ArgumentTuple arg_tp) {
        return invoke<result_type>(m_func, 
            select(std::get<Indices>(m_args), arg_tp)...);
        // select函数根据获取得到的index,从tuple中获取得到对应的参数
    }

private:
    callable_type m_func;
    argument_type m_args;
};

template<typename Func, typename...Args>
inline bind_t<Func, Args...> bind(Func&& f, Args&&...args) {
    return bind_t<Func, Args...>(std::forward<Func>(f), 
            std::forward<Args>(args)...);
}

template<typename Func, typename...Args>
inline bind_t<Func, Args...> bind(Func& f, Args&...args) {
    return bind_t<Func, Args...>(f, args...);
}

std::is_member_function_pointer

用于检查T是否是一个non-static member function pointer。如果是,则返回一个member constant value:true

可能实现:

template<typename T>
struct is_member_function_pointer_helper : std::false_type {};

template<typename T, typename Cls>
struct is_member_function_pointer_helper<T Cls::*> : std::is_function<T> {};

template<typename T>
struct is_member_function_pointer : is_member_function_pointer_helper<
                    typename std::remove_cv<T>::type>> {};

例子:

public:
    void member() {}
};

int main() {
    static_assert(std::is_member_function_pointer<
                        decltype(&A::member)>::value,
                  "A::member is not a memeber function.");
}    

pointer reference

int* m = new int(5);
    int* ptr = m;
    //ptr=new int(6);
    int*& ptr_ref = ptr;
    ptr_ref=new int(6);

    std::cout<< "m=" << *m <<std::endl;
    std::cout<< "ptr="<< *ptr <<std::endl;
    std::cout<< "ptr_ref=" << *(&(*ptr_ref)) << std::endl;

改变ptr的值,将改变ptr_ref的值,而m的值不会发生改变(改变ptr_ref的值的效果一样)。

Pointer to reference

智能指针

C++17之前,shared_ptr不支持动态数组,而unique_ptr支持

std::unique_ptr<int[]> uptr(new int[10]);    //ok
std::shared_ptr<int[]> sptr(new int[10]);    //error

如果试图通过std::shared_ptr来创建动态数组,则需要显式指定删除器(deleter)。例如:

std::shared_ptr<int> p(new int[10], [](int* p){delete[] p;}); //指定delete[]

此外,可以用std::default_delete作为删除器:

std::shared_ptr<int> p(new int[10], std::default_delete<int[]>);

对此,可以封装一个make_shared_array方法让shared_ptr支持动态数组:

template<typename T>
std::shared_ptr<T> make_shared_array(size_t size) {
    return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
int main() {
    std::shared_ptr<int> ptr = make_shared_array<int>(10);
}

std::unique_ptr缺少一个类似于make_sharedmake_unique方法(c++14已经加入了make_unique)。其实要实现一个make_unique方法是比较简单的:

namespace impl {
// 支持普通指针
template<typename T, typename...Args>
inline typename std::enable_if<!std::is_array<T>::value,
            std::unique_ptr<T>>::type make_unique(Args&&...args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
} 

// 支持动态数组。对于动态数组,std::extent的值将为0
template<typename T>
typename std::enable_if<std::is_array<T>::value &&
                            std::extent<T>::value == 0,
            std::unique_ptr<T>>::type make_unique(size_t size) {
    typedef typename std::remove_extent<T>::type U;
    return std::unique_ptr<T>(new U[size]);
}

// 排除定长数组的情况
template<typename T, typename...Args>
typename std::enable_if<std::extent<T>::value != 0,
            void>::type make_unique(Args&&...) = delete;
}

测试:

int main() {
    // Use the default constructor.
    std::unique_ptr<Vec3> v1 = impl::make_unique<Vec3>();
    // Use the constructor that matches these arguments
    std::unique_ptr<Vec3> v2 = impl::make_unique<Vec3>(0, 1, 2);
    // Create a unique_ptr to an array of 5 elements
    std::unique_ptr<Vec3[]> v3 = impl::make_unique<Vec3[]>(5);

    std::cout << "make_unique<Vec3>():      " << *v1 << '\n'
              << "make_unique<Vec3>(0,1,2): " << *v2 << '\n'
              << "make_unique<Vec3[]>(5):   " << '\n';
    for (int i = 0; i < 5; i++) {
        std::cout << "     " << v3[i] << '\n';
    }
}
// make_unique<Vec3>():      {x:0 y:0 z:0}
// make_unique<Vec3>(0,1,2): {x:0 y:1 z:2}
// make_unique<Vec3[]>(5):
//      {x:0 y:0 z:0}
//      {x:0 y:0 z:0}
//      {x:0 y:0 z:0}
//      {x:0 y:0 z:0}
//      {x:0 y:0 z:0}

实现思路:如果不是数组,则直接创建unique_ptr;如果是数组的话,先判断是否为定长数组,如果为定长数组,则编译不通过;如果是非定长数组,则获取数组中的元素类型,再根据传入的参数size创建动态数组的unique_ptrextent<T>::value用来获取数组的长度,如果获取值为0,则说明不是定长数组)。

C++ 17中,可以使用std::shared_ptr管理动态数组。

其中,std::shared_ptr的template parameter需要使用T[N]或者T[]。使用烦那个是:

std::shared_ptr<int[]> ptr(new int[10]);

template<class Y> explicit shared_ptr(Y* p);
Requires: Y shall be a complete type. The expression delete[] p, when T is an array type, or delete p, when T is not an array type, shall have well-defined behavior, and shall not throw exceptions.

Remarks: When T is an array type, this constructor shall not participate in overload resolution unless the expression delete[] p is well-formed and either T is U[N] and Y(*)[N] is convertible to T*, or T is U[] and Y(*)[] is convertible to T*. …

为了支持上述操作,成员类型element_type将被定义为:

using element_type = remove_extent<T>;

此外,还可以通过使用operator[]来访问数组中的元素:

element_type& operator[](ptrdiff_t i) const;

shared_ptrunique_ptr指定删除器方式不同

std:: shared_ptr<int> ptr(new int(1), [](int*p){delete p;}); //ok
std::unique_ptr<int> ptr(new int(1), [](int*p){delete p;});  //error

std::unique_ptr指定删除器的时候,需要确定删除器的类型,所以不能直接像shared_ptr那样指定删除器,可以这样写:

std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [](int*p){delete p;});

这种写法在lambda没有捕获变量的情况下是正确的,如果捕获了变量则会编译报错:

std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [&](int*p){delete p;}); //错误,因为捕获了变量

lambda捕获变量后,unique_ptr报错的原因:lambda在没有捕获变量的情况下,是可以直接转换为函数指针的,捕获了就不能转换为函数指针

如果希望unique_ptr的删除器支持lambda,可以这样写:

std::unique_ptr<int, std::function<void(int*)>> ptr(new int(1), [&](int*p){delete p;});

此外,还可以自定义unique_ptr的删除器。例如:

struct deleter {
    void operator()(int* p) {
        std::cout<< "delete" <<std::endl;
        delete p;
    }
};

int main() {
    std::unique_ptr<int, deleter> p(new int(1));
}

智能指针可以很方便的管理当前程序库动态分配的内存,还可以用来管理第三方库分配的内存。第三方库分配的内存一般需要通过第三方库提供的释放接口才能释放,由于第三方库返回出来的指针一般都是原始指针,如果用完之后没有调用第三方库的释放接口,就很容易造成内存泄露。比如下面的代码:

void* p = GetHandle()->Create();
//do something…
GetHandle()->Release(p);

这段代码可能存在风险,在使用第三方库分配的内存过程中,可能忘记调用Release接口,可能中间不小心返回了,还有可能中间发生了异常,导致无法调用Release接口。这时可以用智能指针去管理第三方库的内存,只要出了作用域内存就会自动释放,不用显式地去调用释放接口,不用担心中途返回或者发生异常导致无法调用释放接口的问题。

void* p = GetHandle()->Create();
std::shared_ptr<void> sp(p, [this](void*p){ GetHandle()->Release(p);});

上面这段代码就可以保证,在任何时候都能正确释放第三方库分配的内存。虽然能解决问题,但还是有些繁琐,因为每个第三方库分配内存的地方都要调用这段代码。所以可以将这段代码提炼出来,作为一个公共函数,简化调用。

std::shared_ptr<void>  Guard(void* p) {
    return std::shared_ptr<void> sp(p, [this](void*p){ GetHandle()->Release(p);});
}
void* p = GetHandle()->Create();
auto sp = Guard(p);
//do something…

上面的代码通过Guard函数做了简化,用起来比较方便,但仍然不够安全,因为有可能使用者可能会这样写:

void* p = GetHandle()->Create();
Guard(p); //危险,这句结束之后p就被释放了
//do something…

这样写是有问题的,会导致访问野指针,因为Guard(p);是一个rvalue,如果不赋值给它一个指针的话,Guard(p);这句结束之后,就会释放,从而导致p提前释放了,后面就会访问野指针的内容。

auto sp = Guard(p);需要一个赋值操作,忘记赋值则会导致指针提前释放,所以这种写法仍然不够安全。可以定义一个宏来解决这个问题:

#define GUARD(P) std::shared_ptr<void> p##p(p, [](void*p){ GetHandle()->Release(p);});

void* p = GetHandle()->Create();
GUARD(p); //安全

或者使用std::unique_ptr

#define GUARD(P) std::unique_ptr<void, void(*)(int*)> p##p(p, [](void*p){ GetHandle()->Release(p);});

【注意】:###运算符

  • #将后面的宏参数进行字符串操作(即将后面的参数用双引号引起来)
  • ##用于连接。

例子:

#define PRINT(NAME) printf("token"#NAME"=%d\n", token##NAME)

PRINT(9);

宏展开为:printf("token"#9"=%d\n",token##9);#9即为"9"token##9即为:token9。整个为:printf("token""9""=%d\n",token9);
所以输出:token9=9;

std::default_delete

定义在<memory>,其原型为:

template< class T > struct default_delete; //使用delete删除分配的单个对象的内存
template< class T > struct default_delete<T[]>; //此特化版本用delete[]来删除数组

std::default_delete是在用户没有指定删除器的情况下,std::unique_ptr(对std::shared_ptr也适用)所采用的的默认删除策略。

通过std::tuple来实现函数调用

template<int...>
struct index_tuple {};

template<int N, int... Indices>
struct make_index : make_index<N-1, N-1, Indices...> {};

template<int... Indices>
struct make_index<0, Indices...> {
    typedef index_tuple<Indices...> type;
};

template<typename Func, int... Indices, typename... Args>
void call_helper(Func func, index_tuple<Indices...>, 
            const std::tuple<Args...>& tup) {
    func(std::get<Indices>(tup)...);
}

template<typename Func, typename... Args>
void call(Func func, const std::tuple<Args...>& tup) {
    call_helper(func, typename make_index<sizeof...(Args)>::type(), tup);
}

int func(int a, int b) {
    std::cout<< a+b <<std::endl;        
}

int main() {
    std::tuple<int, int> tup = std::make_tuple(1,2);

    call<int(int,int), int>(func, tup);
}

类型擦除(type erasure)

类型擦除就是将原有类型消除或者隐藏。需要擦除类型的原因:很多时候并不关心具体类型是什么或者根本就不需要这个类型。

C++中类型擦除方式主要有:

  1. 通过多态来擦除类型
  2. 通过模板来擦除类型
  3. 通过某种容器来擦除类型
  4. 通过某种通用类型来擦除类型

1. 通过多态来擦除类型

这种方式比较常见,通过将派生类型(derived type)隐式转换成基类型(base type),再通过基类去多态地调用行为。

在这种情况下,不用关心派生类的具体类型,而只需要以一种统一的方式去做不同的事情即可。所以把派生类型转成基类型隐藏起来,这样不仅仅可以多态调用,还使程序具有良好的可扩展性。

然而这种方式仅仅是部分的类型擦除,因为基类型仍然存在。此外,这种类型擦除的方式还必须得以继承的方式方式进行,而且继承使得两个对象强烈地耦合在一起。所以,通过多态来擦除类型的方式有较多局限性。

2. 通过模板来擦除类型

通过模板来擦除类型,本质上是把不同类型的共同行为进行抽象。这时,不同类型彼此之间不再需要通过继承(强耦合的方式)去获得共同的行为,而仅仅是通过模板就能获取共同行为,降低了不同类型之间的耦合,是一种很好的类型擦除方式。

然而,第二种方式虽然降低了对象间的耦合,但是还有一个问题没解决:基本类型始终需要指定,并没有消除基本类型。例如,不可能把一个T本身作为容器元素,而必须在容器初始化时就要到T的具体类型。而C++并没有像C#和Java中的object类型,是所有类型的基类。但可以用boost.variant类型来把各种不同的类型包起来,从而可以获得一种统一的类型,而且不同类型的对象间没有耦合关系,它仅仅是一个类型的容器。

struct blob {
    std::string pBuf;
    int size;
};

    typedef boost::variant<double, int, std::string, blob> Value;
    std::vector<Value> ivec;
    ivec.push_back(10);
    ivec.push_back("c++");
    ivec.push_back(1.1);
    ivec.push_back({"java", 4});

上面的代码擦除了不同类型,使得不同的类型都可以放到一个容器中。通过get<T>(Value)便可以获取对应类型的值。

这种方式是通过某种容器把类型包起来,从而达到类型擦除的目的。它的缺点是这个通用的类型必须事先定义好,只能容纳声明的那些类型,增加一种新类型就不行了。

4. 通过某种通用类型来擦除类型

通过这种方式可以消除之前的缺点,其类似于C#和Java中的object类型。这种通用类型是通过boost.any实现的,它不需要预先定义类型,不同类型都可以转成any

实现any

any能容纳所有类型的数据。因此,当赋值给any时,需要将值的类型擦除才行,即以一种通用的方式保存所有类型的数据。

基本思路:可以通过继承去擦除类型(type erasure),基类中不含模板参数,派生类中才有模板参数。这个模板参数类型正是赋值的类型,在赋值时,将创建的派生类对象赋值给基类指针,基类的派生类中携带了数据类型,基类只是原始数据的一个占位符,通过多态,它擦除了原始数据类型。因此,任何数据类型都可以赋值给它,从而实现了能存放所有类型数据的目标。当取数据时,需要向下转换成派生类型来获取原始数据;当转换失败时,打印详情,并抛出异常。由于any赋值时需要创建一个派生类对象,所以还需要管理该对象的生命周期,用unique_ptr去管理对象的生命周期。

struct any {
    any(void) : m_index(std::type_index(typeid(void))) {}
    any(const any& other) : 
        m_ptr(other.clone()), m_index(other.m_index) {}
    any(any&& other) :
        m_ptr(std::move(other.m_ptr)), m_index(other.m_index) {}

    //在创建智能指针时,对于一般的类型,需要通过std::decay来移除引用和cv 
    //qualifier,从而得到原始类型

    //当使用普通类型T(即当T不是已知的的any类型时)创建any时,
    //便使用Perfect forwarding of T
    template<typename T,
            typename = typename std::enable_if<
                !std::is_same<
                    typename std::decay<T>::type,
                    any>::value,
            T>::type
        > any(T&& value) : 
            m_ptr(new derived(value)),
            m_index(std::type_index(
                typeid(typename std::decay<T>::type))) {}

    bool is_null() const {
        return !bool(m_ptr);
    }

    template<typename T>
    bool is_type() const {
        return m_index == std::type_index(typeid(T));
    }

    template<typename T>
    T& any_cast() {
        if(!is_type<T>()) {
            std::cout<< "cannot casting "<< typeid(T).name()
                << " to " <<m_index.name() <<std::endl;
            throw bad_cast();
        }
        auto derived_type = dynamic_cast<derived<T*>*>(m_ptr.get());
        return derived_type->m_value;
    }

    any& operator=(const any& other) {
        if(m_ptr == other.m_ptr) {
            return *this;
        }
        m_ptr = other.clone();
        m_index = other.m_index;
        return *this;
    }
private:
    struct base;
    typedef std::unique_ptr<base> base_ptr;

    struct base {
        virtual ~base() {}
        virtual base_ptr clone() const = 0;
    };

    template<typename T>
    struct derived : base {
        template<typename U>
        derived(U&& value) : m_value(std::forward<U>(value)) {}

        base_ptr clone() const {
            return base_ptr(new derived<T>(m_value));
        }

        T m_value;
    };

    base_ptr clone() const {
        if(m_ptr != nullptr) {
            return m_ptr -> clone();
        }
        return nullptr;
    } 

    base_ptr m_ptr;
    std::type_index m_index;
}; 

扩展:any implementation

strtod

函数原型为:

double strtod( const char* str, const** str_end);

把参数str所指向的字符串转换为一个浮点数(类型为double型)。如果str_end不为空,则指向转换中最后一个字符后的字符的指针会存储在str_end引用的位置。

Imcomplete types(非完整类型)

这是一种缺乏足够信息来判断object大小的object type,一个imcomplete type在translation union的某些阶段可能是completed。

满足以下条件之一的类型是imcomplete:

  • void
  • 未知大小的array type(但在之后的declaration中指定其大小之后便是completed)
  • 未知具体content的struct或union(但在相同作用域之后的declaration中,以相同的struc或者union来定义其content时,便是completed)

fprintf()

int fprintf( FILE *stream, const char *format, [ argument ]...);

其中,streamstdinstdoutstderr
fprintf()函数根据指定的format发送信息(参数)到由stream指定的文件(即,传送格式化输出到一个文件中)。

fprintf()只能和printf()一样工作。fprintf()的返回值是输出的字符数,发生错误时返回一个负值。

Standard Predefined Macros

__FILE__

此宏以C字符串常量的形式扩展为当前输入文件的名称。 这是preprocessor所打开文件的路径,而不是#include中指定的short name或input file name argument。 例如,/usr/local/include /myheader.h是此宏的一种possible expansion。

__LINE__

此宏以十进制整数常量(decimal integer constant)的形式扩展为当前输入行号。 虽然称之为预定义的宏,但它是一个非常奇怪的宏,因为它的“定义”随着每一行新的源代码行的变化而变化。

【注】__FILE____LINE__可用于报告程序检测到inconsistency时,或者当检测到source line的inconsistency时,生成错误信息。例如:

fprintf(stderr, "Internal error:  "
                      "negative string length "
                      "%d at %s, line %d. ",
            length, __FILE__, __LINE__);

__DATE__ & __TIME__

printf("%s",__DATE__);    

__STDC__

一般情况下,此宏扩展为常量1,表示此编译器符合ISO Standard C。如果GNU CPP同GCC以外的编译器一起使用,则结果就一定是这样。但是,除非使用-traditional-cpp选项,否则预处理器将始终conforms to the standard。

如果使用-traditional-cpp选项,则不定义此宏。

在某些主机上,系统编译器使用不同的convention,其中__STDC__通常为0,但如果用户自行指定其严格符合C标准,则为1。 处理系统头文件时,CPP遵循host convention(主机约定),但在处理用户文件时,__STDC__始终为1

__STDC_VERSION__

此宏是C标准的version number,它是一个long integer constant,形式为yyyymmL。其中,yyyymm代表Standard的年份和月份。它表明了编译器所遵循的C Standard。同__STDC__一样,除非连同GNU CPP和GCC一起使用,否则this is not necessarily accurate for the entire implementation。

The value 199409L signifies the 1989 C standard as amended in 1994, which is the current default; the value 199901L signifies the 1999 revision of the C standard; the value 201112L signifies the 2011 revision of the C standard; the value 201710L signifies the 2017 revision of the C standard (which is otherwise identical to the 2011 version apart from correction of defects).This macro is not defined if the -traditional-cpp option is used

__cplusplus

此宏用于C++编译器,可以使用__cplusplus来检测一个header是否能够被当前所使用的C/C++编译器编译。它和__STDC_VERSION__类似。

Depending on the language standard selected, the value of the macro is 199711L for the 1998 C++ standard, 201103L for the 2011 C++ standard, 201402L for the 2014 C++ standard, 201703L for the 2017 C++ standard, or an unspecified value strictly larger than 201703L for the experimental languages enabled by -std=c++2aand -std=gnu++2a.

__ASSEMBLER__

This macro is defined with value 1 when preprocessing assembly language.

macro的技巧

如果宏里有多过一个语句(statement),就需要用do { /*...*/ } while(0)包裹成单个语句,否则就会出现问题:

#define M() a(); b()
if (cond)
    M();
else
    c();

/* 预处理后 */

if (cond)
    a(); b(); /* b(); 在 if 之外     */
else          /* <- else 缺乏对应 if */
    c();

只用 { }也不行:

#define M() { a(); b(); }

/* 预处理后 */

if (cond)
    { a(); b(); }; /* 最后的分号代表 if 语句结束 */
else               /* else 缺乏对应 if */
    c();

do while就行了:

#define M() do { a(); b(); } while(0)

/* 预处理后 */

if (cond)
    do { a(); b(); } while(0);
else
    c();

std::isalnum

定义在<cctype>中,主要用于检查给定字符是否为当前 C 本地环境分类为字母数字字符。

Placement new

简而言之,placement new并不分配内存,而是由使用者给与内存空间来进行构建对象。其形式为:

new (T*) T(...);

第一个括号中的是所给定的指针,其指向足够放下T类型的内存空间。而T(...)则是构造函数的一个调用。

例如,

bool StartArray(){
    new (stack_.template Push<ValueType>()) ValueType(kArrayType);
    return true;
}

可以改写为:

bool StartArry(){
    ValueType* v=stack_.template Push<ValueType>();    //(1)
    new (v) ValueType(kArrayType);
    return true;
}

【注】:在通常情景下,newdelete都是成对出现的,但此例中使用的placement new就通常不需要使用delete,这是因为delete会条用析构函数并释放空间。在这个例子中,stack_提供了内存空间,所以只需要调用ValueType的析构函数即可。

例如:

while(!stack_.Empty())
    (stack_.template Pop<ValueType>(1))->~ValueType();

template disambiguator

对于上述代码中的(1),可得知它只是调用Stack类的模板成员函数Push。如果删掉这个template关键字,就变得令人熟悉:

ValueType* v=stack_.Push<ValueType>();

此处,Push<ValueType>是一个dependent name,它依赖于ValueType的实际类型。于是编译器还不能确定<这个运算符的性质:是小于运算符<还是模板的<。为了避免歧义的产生,需要加入template关键字。这是C++标准规定的,缺少template关键字会导致GCC和Clang报错,而VC会通过。

默认构造函数(default ctor)

1. 什么是default ctor

一般认为,默认构造函数就是编译器自动生成的那个构造函数,但这种理解过于片面。

准确的说,默认构造函数就是在调用时不需要显示地传入实参的构造函数。

根据此原则,下面两种构造函数都是默认构造函数:

class Sample {
public:
    // 默认构造函数。
    Sample() {
        // do something
    }

    // 默认构造函数。虽然有形参,但有默认值,调用的时候可以不显示的传入实参。
    Sample(int m = 10) {
        // do something
    }

    //注:这两种default ctor不应该同时出现,这样会使得编译器产生二义性
};

【注】:default ctor的调用情形:如果如果定义一个对象时没有进行初始化,编译器就会使用default ctor。例如,Sample s;

编译器需要生成default ctor的情形:

  • 当该类的类对象数据成员有默认构造函数时
  • 当该类的基类有默认构造函数时
  • 当该类的基类为虚基类时
  • 当该类有虚函数时

lambda表达式

lambda表达式的声明格式为:

[capture list] (params list) mutable exception-> return type { function body}

上述各个参数的含义:

  1. capture list:捕获外部变量
  2. params list:形参列表
  3. mutable:用于说明是否可以modify捕获的变量
  4. exception:异常设定
  5. return type:返回类型
  6. function body:函数体

其实,对于上面完整的lambda表达式,可以省略其中某些参数,从而获得不完整的lambda表达式。常见的几种形式有:

  1. [capture list] (params list)-> return type { function body }。这种形式声明了const类型的表达式,它不能修改capture list中的值
  2. [capture list] (params list) { function body }。这种形式省略了return type,但compiler可以根据以下规则deduce出lambda表达式的return type
    1. 如果function body中存在return语句,则该lambda表达式的return type就由return语句的返回值类型决定
    2. 如果function type中没有return语句,则lambda表达式的return type就是void类型
  3. [capture list] { function body }。这种形式省略了param list,类似于普通的无参函数

捕获外部变量

lambda表达式可以使用其可见范围内的外部变量,但必须声明哪些外部变量可以被该lambda表达式使用。lambda表达式通过在最前面的[]中来明确指明其内部可以访问的外部变量,这一过程也称为lambda表达式捕获了外部变量。

类似于参数的传递方式(包括值传递引用传递以及指针传递),在lambda表达式中,外部变量的捕获方式有:

  1. 值捕获。被捕获的变量的值在lambda表达式创建时,通过值拷贝的方式传入,因此随后对该变量的修改不会影响lambda表达式中的值。【注】:如果以这种方式捕获外部变量,则在lambda表达式的fucntion body中,不能修改该外部变量的值(read-only)。如果要改变其值,需要加上mutable关键字,使得表达式体内的代码可以更改通过值捕获获得变量。
  2. 引用捕获。只需要在捕获列表变量前面加上一个引用说明符&,引用捕获的变量使用的实际上就是该引用所绑定的对象。
  3. 隐式捕获。隐式捕获有两种方式,分别是[=][&][=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。

例子:

    int a=1;
    auto f=[&a]{std::cout<<a<<std::endl;};
    a=3;
    f();   //3

    int a=1;
    auto f=[&a]{a=2;std::cout<<a<<std::endl;};
    a=3;
    f();   //2

C++11中的lambda表达式捕获外部变量的主要形式有:

捕获方式 描述
[] 不捕获任何外部变量
[variable,...] 默认以值捕获来捕获的多个外部变量(用,分隔),如果引用捕获,需要显示声明(使用&)
[this] 以值捕获来捕获this指针
[=] 以值捕获来捕获所有外部变量
[&] 以引用捕获来捕获所有外部变量
[=,&x] 变量x使用引用捕获,其余变量使用值捕获
[&,x] 变量x使用值捕获,其余变量使用引用捕获

【注】:lambda表达式的参数的一些限制:

  1. 参数列表不能有默认参数
  2. 不支持可变参数
  3. 所有参数必须有参数名
int m=[](int x) {return [](int y) {return y*2;}(x)+6;}(5);      //m=16

[](int x, int y) { return x + y; }(5, 4);  //9

auto Func=[](int x)-> std::function<int(int)>{return [=](int y){return x+y;};};
    auto f=Func(4);
    std::cout<<f(5)<<std::endl; // 9

auto Func1=[](int x)-> std::function<int(int)>{return [=](int y){return x*y;};};
auto Func2=[](const std::function<int(int)>& f,int z){return f(z)+1;};
auto a=Func2(Func1(7),8);  //57

int a=1,b=2;
auto func=[=,&b] () mutable {a=3;b=4;std::cout<<a<<b<<std::endl;};  //3 4
func();
std::cout<<a<<b<<std::endl;  //1 4

std::function<void(int)> f=[](int x){std::cout<<x<<std::endl;};
f(1);

C++11 lambda表达式的实现

其实是编译器为我们创建了一个class,这个class重载了operator()。从本质上讲,lambda只不过是syntatic sugar,可以简化代码。用户书写的lambda表达式与真正的实现为:

对于捕获变量的lambda表达式而言,编译器在创建class的时候,通过成员函数的形式保存了捕获的变量。其形式为:

一个lambda表达式是一个object(这就是为什么说lambda是一个functor,而不是function的原因),所以其拥有一个能够被strore的type。但是,lambda的type只有compiler知道(因为这个type是compiler-generated),所以必须使用auto作为lambda的声明实例的返回类型。

模板的相关问题

一个基础问题:如果在头文件中对模板类进行了声明,如在Types.h中有声明:

template<typename Type>
class Point_ {
    ...
    Point_& operator=(const Point_& pt);
}

则在实现文件Point_.cpp中需要这样写(不能再出现class关键字):

template <typename Type>
Point_<Type> Point_<Type>::operator=(const Point_& pt){}

模板类error LNK2019:无法解析的外部符号

如果将模板类的声明和实现分别写在两个独立的文件中,则在build时就会出现”e“rror LNK2019:无法解析的外部符号“的错误。

解决办法有:

  1. 直接将模板类的中成员函数的声明和定义都放在.h文件中,不要分开就好。
  2. 在使用到该模板类的文件中,既包含模板类的声明文件(.h文件),同时也包含模板类的实现文件.cpp文件。
  3. 在类的定义.h文件的最后包含模板类的实现.cpp文件。

造成这种情况的原因是模板类和模板函数在只有在使用的时候才会被实例化。当模板被使用时,编译器需要函数所有的实现代码,并用合适的类型(即模板参数)来构建正确的函数。但是,如果将函数的实现放在一个单独的源文件中,这些文件就是不可见的,因而会报错。

Anyway, the reason your code is failing is that, when instantiating a template, the compiler creates a new class with the given template argument. For example:

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */ }
};

// somewhere in a .cpp
Foo<int> f;

When reading this line, the compiler will create a new class (let’s call it FooInt), which is equivalent to the following:

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */ }
}

Consequently, the compiler needs to have access to the implementation of the methods, to instantiate them with the template argument (in this case int). If these implementations were not in the header, they wouldn’t be accessible, and therefore the compiler wouldn’t be able to instantiate the template.

#error的使用

编译程序时,只要遇到#error就会跳出一个编译错误。使用它的其目的是:保证程序是按照自己所设想的那样进行编译的

【例子】:
在程序中往往有很多的预处理指令:

#ifdef XXX
...
#else
#endif

当程序比较大时,往往有些宏定义是在外部指定的(如makefile),或是在系统头文件中指定的。当不太确定当前是否定义了XXX时,就可以改成如下这样进行编译:

#ifdef XXX
...
#error "XXX has been defined"
#else
#endif

这样,如果编译时出现错误,就会输出XXX has been defined,表明宏XXX已经被定义了。

重载<<>>操作符

在开始重载这些操作符之前,必须注意:这些操作符必须重载为全局函数。如果想要让它们访问私有成员,则必须设置为友元(friend)

必须将这两个操作符重载为全局函数的原因:如果操作符重载为一个成员函数,则它必须是对象的成员,且出现在操作符的左侧。由于<<的调用方式为:cout << obj;,所以如果将它重载为一个成员函数,并且对象能够出现在操作符的右侧,则我们需要将obj定义为ostream类的成员,显然这是不现实的。因此,一般将这些操作符重载为接受两个参数的全局函数,其中一个参数为cout,另一个则是自定义类的对象

例子:

#include <iostream>
class Complex {
private:    
    int real, imag;
public:    
    Complex(int r = 0, int i =0)   {  real = r;   imag = i; }    
    friend std::ostream & operator << (std::ostream &out, const Complex &c);    
    friend std::istream & operator >> (std::istream &in,  Complex &c);
}; 
ostream & operator << (std::ostream &out, const Complex &c) {    
    out << c.real;    
    out << "+i" << c.imag << endl;    
    return out;
} 

istream & operator >> (std::istream &in,  Complex &c) {    
    std::cout << "Enter Real Part ";    
    in >> c.real;    
    std::cout << "Enter Imaginary Part ";    
    in >> c.imag;    
    return in;
} 

int main(){   
    Complex c1;   
    std::cin >> c1;   
    std::cout << "The complex object is ";   
    std::cout << c1;   
    return 0;
}

setjmplongjmp用法

setjmplongjmp是C语言独有的,只有将它们结合起来使用,才能达到程序控制流有效转移的目的,按照用户的预先设计的意图,去实现对程序中可能出现的异常进行集中处理。

这两个函数的原型定义在<setjmp.h>中:

int setjmp(jmp_buf envbuf);

setjmp函数用缓冲区envbuf保存系统堆栈的内容,以便后续的longjmp函数使用。setjmp函数初次启用时返回0值。

void longjmp(jmp_buf envbuf, int val);

longjmp函数中的参数envbuf是由setjmp函数所保存的堆栈环境,参数val设置setjmp函数的返回值。longjmp函数本身是没有返回值的,它执行后跳转到保存envbuf参数的setjmp函数调用,并由setjmp函数调用返回,此时setjmp函数的返回值就是val

通俗的解释是:先调用setjmp,用变量envbuf记录当前的位置,然后调用longjmp,返回envbuf所记录的位置,并使setjmp的返回值为val。当时用longjmp时,envbuf的内容被销毁了。其实这里的“位置”一词真正的含义是栈定指针

例子:

#include <stdio.h> 
#include <setjmp.h>

jmp_buf buf;

void banana() { 
    printf("in banana() \n"); 
    longjmp(buf,1);

    printf("you'll never see this,because i longjmp'd");
}

int main() { 
    if(setjmp(buf)) 
        printf("back in main\n"); 
    else { 
        printf("first time through\n"); 
        banana(); 
    }
}

//first time through
//in banana()
//back in main

setjmp/longjmp的最大用处是错误恢复,类似try ...catch...。其功能比goto强大很多,goto只能在函数体内跳来跳去,而setjmp/longjmp可以在到过的所有位置间进行跳转。

重载operator->

重载operator->必须定义为类的成员函数。此外,不管其形式如何,->表达式不接受显示的形参。

对于形如pointer->mem的表达式而言,pointer必须以下两种形式之一:

  • 指向类对象的指针
  • 重载了operator->()的类对象

根据pointer类型的不同,有以下两条规则:

  1. 如果pointer是指针,则按照内置的->运算符去处理。表达式等价于(*pointer).mem。即,先对pointer指针进行解引用,然后从所得的对象中获得指定的成员。如果没有所指定的成员,编译器则会报错。
  2. 如果pointer是一个定义了operator->()的类对象,则pointer->mem等价于pointer.operator->()->mem
    • 如果operator->()的返回结果是一个指针,则转到1;
    • 如果operator->()的返回结果是一个对象,且该对象也重载了operator->(),则重复调用2,否则编译器报错。最终,过程要么结束在1,要么无限递归下去亦或是编译器报错。

简而言之,如果返回类型是类类型(或者这种对象的引用),则将递归引用该操作符。编译器检查返回对象所属类型是否具有成员->运算符。如果有,则直接应有那个操作符;否则,编译器将产生一个错误。这个过程将持续下去,直到返回一个指向带有指定成员的对象。

例子,

class A{
public:
    int i;
    A(){i=100;}
    void print(int a){printf("%d\n", a);}
    A& operator->(){return *this;}
};

int main(){
    int ret;
    A a;
    ret=a->i;
    a->print(200);
}

编译器将会报错:error: circular pointer delegation detected。因为上面的代码将会让operator->()返回对象本身,从而导致无穷递归,最后报错。

而如果改为如下,则能够得到正确的结果:

class A{
public:
    int i;
    A(){i=100;}
    void print(int a){printf("%d\n", a);}
    A* operator->(){return this;}
};

int main(){
    int ret;
    A a;
    ret=a->i;
    a->print(200);
}

Debug与Release的区别

Debug 和 Release 并没有本质的区别,他们只是VC预定义提供的两组编译选项的集合,编译器只是按照预定的选项行动。如果我们愿意,完全可以把Debug和Release的行为完全颠倒过来。

Debug和Release,主要是针对其面向的目标不同的而进行区分的。Debug通常称为调试版本,通过一系列编译选项的配合,编译的结果通常包含调试信息,而且不做任何优化,以为开发人员提供强大的应用程序调试能力。而Release通常称为发布版本,是为用户使用的,一般客户不允许在发布版本上进行调试。所以不保存调试信息,同时,它往往进行了各种优化,以期达到代码最小和速度最优,为用户的使用提供便利。

VC中的“烫烫烫”与“屯屯屯”

在Debug模式下,栈区开辟的存储空间都是使用0xCCCCCCCCH来填充4字节单位的。也就是说,栈区开辟的存储局部变量的空间的每一个字节都被0xCC填充。为什么使用0xCC?这是因为它是INT 3H的机器码,起保护作用,也可以用作下断点),而两个0xCC合起来就正好输出中文“烫”字。与此同时,动态分配的空间开辟给堆,而VC的Debug模式下,用0xCD填充堆的空间,而两个0xCD合在一起就是中文“屯”字。这样做的目的是可以让用户方便地看出哪些内存没有进行初始化

简而言之,在VC总共遇到栈中或者堆中申请的空间没有被初始化就会显示“烫烫烫”或者“屯屯屯”。

【注】:在Release模式下,则不会有这种附加的动作,原来那块内存中是什么就是什么。

Memory Debug Codes

对于C runtime library提供了其debug codes:

  • 0xCD0xCDCDCDCD:New objects。当New objects被allocate地时候,将被填充到0xCD中。
  • 0xFD0xFDFDFDFD:No-man’s land memory。其属于已分配内部块的额外字节,但不是用户申请的内存区域。 它们被放置在申请内存区域之前和之后,用于数据绑定检查。
  • 0xDD0xDDDDDDDD:Freed blocks。当设置_CRTDBG_DELAY_FREE_MEM_DF标志时,如果已释放的内存块在调试堆(debug heap)链表中仍呈未使用状态,则用0xDD进行填充。 虽然在某些情况下用户不会看到0xDDDDDDDD值,因为它会被另一个调试函数覆盖(例如,HeapFree0xFEEEFEEE)。

上述这些常量定义在<DbgHeap.c>中:

static unsigned char _bNoMansLandFill = 0xFD; /* fill no-man's land with this */
static unsigned char _bDeadLandFill = 0xDD; /* fill free objects with this */
static unsigned char _bCleanLandFill = 0xCD; /* fill new objects with this */

编译器初始化

0xCC0xCCCCCCCC)—VC++编译器会初始化未由程序显式初始化的所有局部变量。 它使用0xCC0xCCCCCCCC填充这些变量使用的所有内存。

例如,在栈中申请一个大小为4个字节的字符数组:

int main() {
    char ch[4];
    return 0;
}

打下断点进行查看:


进一步查看反汇编:

int main(void){
00007FF78E4C16E0  push        rbp  
00007FF78E4C16E2  push        rdi  
00007FF78E4C16E3  sub         rsp,108h  
00007FF78E4C16EA  lea         rbp,[rsp+20h]  
00007FF78E4C16EF  mov         rdi,rsp  
00007FF78E4C16F2  mov         ecx,42h      //rep的循环次数为42h
00007FF78E4C16F7  mov         eax,0CCCCCCCCh //将栈空间的42h个双字节赋值为0CCCCCCCCh 
00007FF78E4C16FC  rep stos    dword ptr [rdi]  
    char x[4];
    return 0;
00007FF78E4C16FE  xor         eax,eax  
}

Callable objects

Callable object(可调用对象)是可以像函数一样被调用的任何对象的通用名称:

  • A member function (pointer)
  • A free function (pointer)
  • A functor
  • A lambda

在C语言中,有指向函数的指针的概念,它允许存储任何函数的地址(提供与指针匹配的签名)。 但是,指针函数与指向成员函数的指针具有不同的签名; 它们可以作为lambda的不同签名。

std::function是一个模板类,可以保存与其签名匹配的任何可调用对象。std::function为存储,传递和访问这些对象提供了一致的机制。

如果可调用对象与std::function的签名匹配,则std::function可以被认为是指向任何可调用对象的通用函数指针。 而且,与C语言的函数指针不同,C++编译器对可调用对象的参数(包括返回类型)提供强类型检查。std::function重载了operator !=允许它与nullptr进行比较(因此它可以像函数指针一样)。

std::clock

定义在<ctime>中,其中,clock_t的定义为:

#ifndef _CLOCK_T_DEFINED
typedef long clock_t;
#define _CLOCK_T_DEFINED
#endif

从上面代码可以看出,clock_t是一个长整形数。在<ctime>文件中还定义了一个常量CLOCKS_PER_SEC,它用来表示每一秒钟会有多少个时钟计时单元。其定义为:

#define CLOCKS_PER_SEC ((clock_t)1000)

clock()返回的单位是毫秒,如果想要返回以秒为单位,则可以使用:

duration = (finish - start) / CLOCKS_PER_SEC;

简单而言,就是该程序从启动到函数调用,这整个过程所占用CPU的时间。std::clock可能前进快于或慢于wall-clock,这取决于操作系统给予程序的执行资源。例如,若与其他进程共享CPU,则std::clock时间的前进可能慢于wall-clock。另一方面,若当前进程为多线程,且多于一个执行核心可用,则std::clock时间的前进可能快于wall-clock。