C++11类的新语法和特性

Posted OshynSong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11类的新语法和特性相关的知识,希望对你有一定的参考价值。

sizeof计算类成员大小

sizeof可以直接用于获取Class::member的大小,而不用通过Class的实例获取。

class A                               
public:                                
    long long llint = 0;
;

cout << sizeof(A::llint) << endl;

上述测试输出类A的long long类型的成员占用8个字节。

default关键字扩展

新标准扩展了原有default关键字的用法,可以使用default强制编译器生成默认的构造函数、析构函数、复制构造函数、拷贝赋值函数、移动构造函数、移动拷贝函数。

class TestClass

public:
    TestClass() = default;
    TestClass(const int i, const char c): member_i(i), member_c(c) 
    TestClass(const TestClass&) = default;

    int member_i;
    char member_c;
;

TestClass tc; // may cause error if no default construct.

TestClass tc1(1, 'a'); // by defined constuctor

上述的类定义中由于用户自定义了一个构造函数,此时编译器就不会合成无参数的默认构造函数了,这样就无法使用TestClass tc这样的语句定义对象。使用default关键字可以强制生成一个无参默认构造函数。

delete关键字扩展

新标准扩展了原有delete关键字的用法,可以使用delete强制编译器不合成默认的构造函数、析构函数、复制构造函数、拷贝赋值函数、移动构造函数、移动拷贝函数。

final 与 override

新标准中提供了override和final两个关键字,用于标识子类对父类中虚函数的重写(override)或禁止重写(final)。

 class A  
 public:
     A() = default;
     virtual void f()
         std::cout << "parent class f" << std::endl;
        
     virtual void g() final 
         std::cout << "parent class g" << std::endl;
        
 ;   

 class B : public A  
 public:
     void f() override
         std::cout << "derived class f" << std::endl;
        

     //编译错误
     //error: virtual function ‘virtual void B::g()’ overriding final function ‘virtual void A::g()’
     void g() override 
         std::cout << "derived class g" << std::endl;
     

     //编译错误
     //error: ‘void B::g(int)’ marked ‘override’, but does not override
     void g(int) override 
         std::cout << "derived class g" << std::endl;
        
 ;

从实例中可以看出两种情况下编译器都会报错:

  • 标识final的函数被子类重写
  • 标识了override的函数实际上没有重写父类中的函数

同时,final也可以用在类名后面用来限定类不能被继承。如下:

class FinalClass final 
public:
  //FinalClass不能被继承
  //...
;

构造函数

同类委托
可以在初始化列表中将一个constructor初始化的工作委托给该类的另一个constructor。这种方式在使用模版构造函数时非常有效:

class A 
public:
    template<typename I> A(I beg, I end);
    A(vector<int> &avec): A(avec.begin(), avec.end()) ;
    A(list<int> &alist): A(alist.begin(), alist.end()) ;
    //....
;
template<typename I> A::A(I beg, I end) 
    /// 实际初始化工作
    /// ...

上述用法在提供多类构造函数的时候非常有效。


父类委托
同时,也支持子类在初始化列表中直接委托父类的构造函数完成初始化。

class A
public:
    A() = default;
    A(int a, int b);
;

class B : public A 
public:
    B(int b) : A(0, b) 
;

调用父类A的两参数构造函数。


多重继承
多重继承的子类可以直接继承父类的构造函数,但是如果父类中有形参列表完全相同的构造函数,则会产生冲突,这时需要子类自己定义一个自己版本的构造函数。


allocator.construct支持任意构造函数
新标准中,allocator.construct可以使用任意的构造函数。

class A
public:
    A(int i) : _mem(i) 

private:
    int _mem;
;

allocator<A> alloc;
auto p = alloc.allocate(10); //为类A的一个对象分配内存,p为指向该内存的指针
alloc.construct(p, 10); //调用A的单参数构造函数

移动(move)语义

右值是一个行将销毁的值。新标准中允许通过&&标识定义一个右值引用,将其绑定到一个右值上。
新标准提供了std::move函数,就是获得一个左值的右值引用,这样就找到了一种途径将一个右值引用绑定到一个左值上。但是,使用std::move也意味着交出左值的控制权,之后就不能再使用这个左值了,因为使用std::move之后,无法对这个左值做任何保证。


移动构造与赋值
就是接受一个右值引用的构造函数。接受该右值引用所引用的对象,而没有实际的大块内存拷贝操作。调用移动构造函数的关键是要传入一个相应的右值引用,这时上面提到的std::move函数就派上用场了。
新标准规定:调用移动构造函数之后,右值引用所绑定的对象保证可析构可销毁的状态。
对于含有指针的类,之前可以使用深复制的方式进行构造和拷贝,但是涉及到大量的内存拷贝操作。新标准提供的移动构造就能避免这一点,相当于*转换了将右值引用绑定的对象的所有权转交给新对象

class A

public:
    A() = default;
    A(size_t cap):capacity(cap), size(0), pointer(nullptr) 
        pointer = new int[cap];
    

    A(const A &rhs):capacity(rhs.capacity), size(rhs.size), pointer(nullptr) 
        pointer = new int[capacity];
        for (int i = 0; i < size; ++i)
            pointer[i] = rhs.pointer[i];
        
    

    //深复制进行拷贝
    A &operator=(const A &rhs) 
        if (this == &rhs) 
            return *this;
        
        A tmp(rhs); //copy-and-swap方法
        capacity = tmp.capacity;
        size = tmp.size;
        int * tmpp = tmp.pointer;
        tmp.pointer = pointer;
        pointer = tmpp;
        return *this;
    

    A(A&& rhs) noexcept:capacity(rhs.capacity), size(rhs.size), pointer(rhs.pointer)
        rhs.pointer = nullptr;
        rhs.capacity = 0;
        rhs.size = 0;
    

    A &operator=(A&& rhs) & noexcept 
        if (this == &rhs) 
            return *this;
        
        capacity = rhs.capacity;
        size = rhs.size;
        if (pointer) 
            delete []pointer;
        
        pointer = rhs.pointer;

        if (rhs.pointer) 
            delete []rhs.pointer;
            rhs.pointer = nullptr;
        
        rhs.capacity = 0;
        rhs.size = 0;
        return *this;
    

    ~A() 
        if (pointer) 
            delete []pointer;
            pointer = nullptr;
        
    

    //...

private:
    size_t capacity;
    size_t size;
    int *pointer;
;

在含有指针对象的类中,复制构造和赋值运算符函数都是用了深复制,进行了堆内存的重新申请和拷贝。而在移动构造函数里,只需要窃取指针及其状态,并将右值引用对象的状态重置,即可完成移动构造的操作。同理,在移动赋值运算符函数中,首先将自身对象的指针析构,然后窃取右值引用参数的指针为己所用,并将右值引用所引用的对象置空,从而没有发生堆内存的申请和拷贝。
两个移动函数都添加了noexcept标识符。这也是C++11新标准中引入的,用于向标准库指明此函数不会抛出异常,以避免标准库在和我们定义的这个类进行交互时做一些不必要的工作。如果我们不承诺noexcept,那么当标准库容器扩展容量时,就不能调用移动构造函数来移动容器内的现存元素,而只能采取比较耗费资源的拷贝构造函数。


引用折叠
当左右引用遇到模板参数的时候,需要用到引用折叠规则来获得最终的模板推断类型和形参类型:除了T&& &&折叠为T&&之外的所有情况均折叠为T&
下面是std::move的定义:

template<typename T>
typename remove_reference<T>::type&& move(T &&t)

  return static_cast<typename remove_reference<T>::type&&>(t);

参数类型是T&&,看上去是个右值引用,但是实际上也是可以接受左值引用的类型的:

  • 当传入一个右值引用时,依据引用折叠规则T && &&会解析成T &&
  • 当传入一个左值时,编译器会依然使用折叠规则推断T&& &T&

从而这个函数对两种引用都能处理。但是返回值确实一个右值引用,从而如果传入的参数为左值引用就进行了类型转换,而传入的参数若为右值引用则不变。

同理,当模版形参为右值引用时,本身为一个变量,如上述move函数的形参T &&t,此时如果希望获取原来实参的类型信息,可以使用std::forward。示例如下:

void f(int &&i)

    std::cout<<i<<"\\t i is a right ref.\\n";


void g(int &i)

    std::cout<<i<<"\\t i is a left ref.\\n";


template <typename F, typename T>
void forward_test(F f, T&& val)

    f(std::forward<T>(val));


int n = 2;
int && rref = n;
int & val = n;
forward_test(f, 5);  //输出:5 i is a right value
forward_test(g, rref); //输出:2   i is a left value
forward_test(g, val);  //输出:2   i is a left value

以上是关于C++11类的新语法和特性的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 的新特性:类的 #private 字段

jdk11新特性——局部变量类型推断(var ”关键字”)

jdk11新特性——局部变量类型推断(var ”关键字”)

C++11新特性

C++开发者都应该使用的10个C++11特性

C++11 现代C++风格的新元素--简介