深入理解C++11 读书笔记(三) 通用与专用

本文深入探讨C++11的新特性,包括继承构造函数的使用,如何通过委派构造函数减少重复代码,以及移动语义和完美转发的概念。移动语义利用右值引用优化资源转移,完美转发则解决了函数模板参数传递的问题。此外,还介绍了显示转换操作符、初始化列表和POD类型的定义与优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继承构造函数

  • 如果派生类并不需要改变构造函数,那么可以用新的using Base::Base直接继承基类的构造函数,基类构造函数很多的时候,这样做非常方便。如果派生类还有自己的成员需要初始化,可以利用类成员初始化表达。
class A{
    A(int a):a(a){}
    int a;
};

class B:A
{
    using A::A
}
  • 使用继承构造函数,编译器不会产生真正的目标代码,可以节约目标代码控件。
  • 继承构造函数参数的默认值不会被继承,而且会产生多个继承构造函数。A(int a = 1,double = 2)的继承类使用using A::A会产生B(int,double);B(int);B();B(const B &);多个构造函数。因此,如果父类有默认参数的构造函数,使用继承构造需要很小心。

委派构造函数

  • 在自己的构造函数里调用自己另外的构造函数,以便减少重复代码。
class A{
    A(){init();}
    A(int a):{init();this->a = a;}
    A(char c):{init();this->c = c;}
};
  • 使用tricky一点的方法是利用placement new
class A{
    A(){init();}
    A(int a):{ new(this)A();this->a = a;}
    A(char c):{new(this)A();this->c = c;}
};
  • 在C++11中使用委派构造函数
class A{
    A(){init();}
    A(int a):A(){ this->a = a;}
    A(char c):A(){this->c = c;}
};
  • 区别调用者和被调用者,A(int a)被称为 委派构造函数(delegate constructor),A()被称为 目标构造函数(target constructor)。
  • 委派构造函数和初始化列表不能同时使用。比如A(int):A(),a(a){}是非法的,初始化代码需要放到函数体。
  • 委派构造函数互相调用会形成链状,但是不能成环,会导致编译错误。

移动语义和完美转发

  • 移动语义:利用右值临时变量对变量进行拷贝构造时,变量偷走有时右值变量构造函数里创建的堆上的资源。右值临时变量是即将消失的变量,拿走堆上的资源不会产生问题,减少堆的拷贝。
class A{
    A(){ d = new Data;}
    A(const A & a):d(new Data(a.d)){}//普通拷贝
    A(A && a):d(a.d){  a.d = nullptr; }//右值引用的拷贝
    Data * d;
}
  • 左值:可以取地址的值,有名字的值。int a; &a合法,左值
  • 右值:不能取地址的值,没有名字的值。int b,c; &(b+c)非法,(b+c)是临时变量,是右值。右值由两个概念构成:将亡值(xvalue,eXpiring value). 和 纯右值(prvalue, pure Rvalue)。2, true, ‘c’,lambda表达式,类型转换返回值都是右值。 返回右值引用的表达式 T&& , std::move的返回值,转换为T&& 类型的值 都是右值。C++11所有的值必然是 左值,将亡值,纯右值之一。
  • 右值引用为了区别C++98里的引用,将98里称为左值引用,左值引用是有名字变量的引用,右值引用是匿名变量的引用。
    T getValue(){ T t; return t;}

    getValue(); //返回值临时变量立马被析构
    T && a = getValue();   //右值引用临时变量,临时变量暂时不会被析构
    const T & a = getValue();    //有效,常量左值引用可以引用临时变量
    T & a = getValue();   //非法,非常量左值引用不能引用右值
  • 因此,右值引用有延长临时变量生命周期的作用,常量左值应用也能延长临时变量生命周期,但是不能更改。
  • 标准库<type_traits>提供三个模板类,可以判断是否是左值引用还是右值引用、引用类型。
    is_rvalue_reference,is_lvalue_reference,is_reference
    cout<<is_rvalue_reference<string &&>::value;
    std::move
  • C++11 在<utility>里提供强制转换函数,从实现上将,基本等同于一个类型转换函数static_cast<T&&>(lvalue)
  • T t1 = std::move(t2),如果T没有实现移动构造,将会调用拷贝构造函数。
  • C++11中拷贝/移动构造有三个版本T(const T &);T(T&);T(T&&)移动构造一般要用来偷数据,因此默认没有const版本。
  • move_if_noexcept(t):如果t的移动构造有noexcept修饰,则返回右值,没有noexcept修饰则返回左值,调用拷贝构造。
  • RVO/NRVO (return value optimization) 优化,是g++/clang的编译选项,有时会优化掉构造和移动,若要正确实验移动构造的一些demo时,就关掉这个选项。
    完美转发
  • 函数模板中,完全按照模板的参数类型,将参数传递给函数内另一个函数。由于内部函数接收的参数可能是const、右值引用等,就得需要实现多个版本的模板,为了解决这个这个问题,C++11引入引用折叠。
    C++11之前可能需要这样的处理
void inner(const char *);
template<class T>
void iamforwarding(const T &t){inner(t);}
void iamforwarding(T & t){inner(t);}
void iamforwarding(T && t){inner(t)}
  • 引用折叠(reference collapsing)
typedef const int T;
typedef T & TR;
TR & v = 1; //c++98编译错误,c++11引用折叠

折叠规则:只要定义中出现左值,就是左值。
这里写图片描述

因此可以如此书写

template<class T>
void iamforwarding(T && t)
{
    inner(static_cast<T &&>(t));
}

这里如果实参是X的左值引用,T推导为X&, T&&推导为 X& &&,折叠后结果还是X&. 而如果是X的右值引用,T&&推导为 X&& &&, 结果是X&&。因此实现了完美转发。注意记录右值引用的变量本身确实左值,即int && a = 2;a本身是左值,作为参数调用函数时是会调用左值的版本,因此inner需要把t转化为右值。

  • 完美转发中static_cast
template<class T>
void iamforwarding(T && t)
{
    inner(forward(t));
}

显示转换操作符

  • 构造函数前加上explicit,可以禁止隐式类型转换。而在C++11里,explicit的使用范围扩大到自定义类型转换。
template<class T>
class Ptr{
    Ptr(T * p):p(p){}
    bool operator() const {return p!= nullptr;}
    T * p;
}

这里加入bool 类型转换后,完美可以直接使用if(p){},但同时也让p1+p2在语法上成立,为了避免这种隐式转换,加上explicit

explicit bool operator() const {return p!= nullptr;}

初始化列表

  • c++98里可以对数组用花括号统一列表初始化,但在vector等集合里的数据就没法通过这样的方式初始化,c++11允许了这样的初始化方式,称为初始化列表(initializer list)。花括号前加不加等号 效果都相同。
    int a[]{1,2,3}; int a[] = {1,2,3}
  • 使用initializer_list模板来自定义初始化列表的初始化逻辑。在头文件<initializer_list>内定义。
class A{
    A(initializer_list<pair<char c,int i>> l){
        for(auto it = l.begin(); it != l.end(); ++it){
            data.push_back(*it);
        }
    }
    vector<pair<char,int>> data;
}
  • 初始化列表能够防止类型收窄(会产生数据变化或精度丢失的隐式转化),产生类型收窄时会编译错误
    int a{2.0}//收窄,
    char a = 127;//收窄,可以编译通过。
    char b{127};//收窄,不能编译通过

POD (Plain Old Data)

trivial 平凡的要求:

  1. no virtual functions or virtual base classes
  2. no base classes with a corresponding non-trivial constructor/operator/destructor
  3. no data members of class type with a corresponding non-trivial constructor/operator/destructor

如果有自定义的构造函数,那么必须显示提供default版本的构造函数。(个人感觉就是保证没有虚表指针,内存里都是具体数据,不清楚自定义构造/拷贝/操作符 函数和缺省函数对数据layout的影响)
trivial保证了数据连续分布,因为有对齐操作,可能数据之间有padding,能保证C++里数据之间的内存拷贝,但是不能保证和C里是一致的。

standart layout 标准布局的要求

  • no virtual functions or virtual base classes
  • all non-static data members have the same access control
  • all non-static members of class type are standard-layout
  • any base classes are standard-layout has no base classes of the same type as the first non-static data member.
  • meets one of these conditions:
    • no non-static data member in the most-derived class and no more than one base class with non-static data members, or
    • has no base classes with non-static data members
  • 基类和派生类只有一个有非静态成员
  • 访问权限要相同(public,private,protected)
  • 第一个成员不能是基类类型。
struct B{};
struct D1:B{//非标准类型
    B b;
}d1;
struct D2:B{标准类型
int a;
B b;
}d2;

这条规则是基于C++允许优化不包含成员的基类产生的。在C++标准中,当基类没有成员时,标准允许派生类第一个成员和基类共享地址&d2 == &d2.a,可是当第一个成员和基类类型相同时,C++标准要求类型相同的对象必须地址不同,于是编译器仍然会给基类分配一个字节空间long long(&d1) + 1 == long long(&d1.b)

POD要求同时满足trivial和standard layout
可以使用辅助类判断是否是trivial,standard layout或pod

template<typename T>
struct std::is_trivial;
template<typename T>
struct std::is_standart_layout;
template<typename T>
struct std::is_pod;

pod的好处
1. 字节赋值,安全使用memset和memcpy对pod类型初始化和拷贝
2. 提供对C兼容,数据在c和c++之间操作总是安全的
3. 保证静态初始化的安全有效。pod类型对象初始化可以放入目标文件.bss段,在初始化中直接赋值为0


非受限联合体(unrestricted union)

  • c++11在union上对98进行了很多改进,任何非引用类型都可以作为联合体数据,可以有静态函数,不能有静态变量。
  • 如果非pod成员有非平凡的构造函数,那么联合体默认构造函数将会被删除,其他特殊成员函数(拷贝,移动,析构)遵循此规则。
unit T{
string s;
int n;
};
void main(){
T t;//构造失败,string有非平凡构造函数,T的默认构造函数被删除
}

因此,这种情况下我们要自己定义非受限联合体定义的构造函数,通常情况下,要使用placement new操作符。

unit T{
string s;
int n;
T(){ new(&s)string;}
~T(){s.~string();}    //注意,析构的时候要保证t是一个string对象
};
void main(){
    T t;//构造成功
}

用户自定义字面量

  • 通过定义一个类的字面量操作符,就可以通过带后缀的字符串定义一个临时对象
class RGBA
{
RGBA operator ""_C (char * ch,int cnt){
    //省略对字符串的解析
    //code...
    return *this;
}
int clr[4]
};
void blend(RGBA && c1,RGBA && c2);
void main(){
    blend("r211 g0 b0 a123"_C,"r233 g32 b12 a34"_C);
}

C++字面量规则
* 如果字面量是整数,字面量操作符可接受unsigned long long 或 const char * (结尾自动添加’\0’)为参数。
* 如果字面量是整数,字面量操作符可接受long double 或 const char * 为参数。
* 如果字面量是字符串,字面量操作符可接受(const char *,size_t)为参数
* 如果字面量是字符,字面量操作符接受char为参数。
注意
* 操作符声明时operator”“与自定义后缀之间有空格
* 后缀要以下划线开始,否则编译器有警告,否则定义L这种会有混淆。


内联名字控件

命名空间下有子空间,使用时不是那么方便。
* namespace前加上inline关键字,可以让该命名空间定义的模板可以在父命名空间里进行特化。

namespace Jim{
namespace Basic{
struct Knife{Knife(){cout<<"Knife in Basic."<<endl;}};
class CorkScrew{};
}
namespace Toolkit{
templatetypename T>class SwissArmyKnife{};
}
//...
namespace Other{
//...
}
//打开一些内部名字空间
using namespace Basic;
using namespace Toolkit;
}
template<>class SwissArmyKnife<Knife>{};//编译失败
}
using namespace Jim;
int main(){
SwissArmyKnife<Knife>sknife;
}
//编译选项:g++3-9-2.cpp

上面这个会编译失败

#include <iostream>
using namespace std;
namespace Jim{
inline namespace Basic{
struct Knife{Knife(){cout<<"Knife in Basic."<<endl;}};
class CorkScrew{};
}
inline namespace Toolkit{
templatetypename T>class SwissArmyKnife{};
}
//...
namespace Other{
Knife b;//Knife in Basic
struct Knife{Knife(){cout<<"Knife in Other"<<endl;}};
Knife c;//Knife in Other
Basic::Knife k;//Knife in Basic
}
}
//这是LiLei在使用Jim的库
namespace Jim{
template<>class SwissArmyKnife<Knife>{};//编译通过
Jim;
int main(){
SwissArmyKnife<Knife>sknife;
}
//编译选项:g++ -std=c++11 3-9-3.cpp

这样的就编译通过。

使用inline命名空间会破坏良好的分隔,不过这跟inline的使用方式有关

#include <iostream>
using namespace std;
namespace Jim{
#if__cplusplus==201103L
inline
#endif
namespace cpp11{
struct Knife{Knife(){cout<<"Knife in c++11."<<endl;}};
//...
}
#if__cplusplus<201103L
inline
#endif
namespace oldcpp{
struct Knife{Knife(){cout<<"Knife in old c++."<<endl;}};
//...
}
}
using namespace Jim;
int main(){
Knife a;//Knife in c++11.(默认版本)
cpp11::Knife b;//Knife in c++11.(强制使用cpp11版本)
oldcpp::Knife c;//Knife in old c++.(强制使用oldcpp11版本)
}
//编译选项:g++ -std=c++11 3-9-4.cpp

模板别名

  • 使用using比使用typedef更加灵活
typedef unsigned int uint;
using uint = unsigned int;

使用using更符合语言逻辑,使用typedef的地方都可以用using,而using还有更多的使用方式

template<typename T>
using MapString = std::map<T,char *>;

MapString<int> numberedString

sfinea规则

(substitution failure is not an error) 匹配失败不是错误
* 略

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值