870920 Menu

SwingCoder之C++备忘录·18

C++ 11

C++ 11做了多项改进,比较重要的列举如下。

1、类内初始化数据成员:
class MyClass
{
private:
int intNum = 0; // 可在类内初始化数据成员,相当于给出数据成员的默认值
// 使用无“=”的大括号初始化语法,这也是C++ 11新增的初始化语法
String name {“John”};
}
注意:如果构造函数中给出了初始化列表,则初始化列表中的值将改写类内初始化的默认值。
2、基于范围的for循环:
对于内置类型和包含begin()和end()的数组,可采用新的for语法进行数组中所有元素的遍历:
double prices[] = {0.3, 1.5, 3.2, 6.6, 8.49};
for (auto x : prices) // auto也是C++ 11新增的声明语法
std::cout << x << std::endl;
这种循环方式,x依次为数组prices中的每个元素,相当于const prices[i]。如需修改数组中的元素,则写为:for (auto& x : prices) // prices可以是C++内置数组,也可以是标准库中的容器
3、右值引用:
// 该函数返回值而非返回引用
double areaOfCircle(double x) { return x * x * 3.14159265358979; }
double&& rightRefer = areaOfCircle(5.0); // rightRefer为右值引用型变量
要理解右值引用,必须搞清楚C++的“左值”和“右值”这两个基本概念。每个C++常量、变量、表达式、函数的返回值,都有明确的数据类型,并且都有内存空间用来存储该数据。所谓变量名和表达式,只是指代该数据的一个或一串文本标识符。左值是在该名称作用域内一直存在的变量或常量,而右值则是临时使用完之后即不再存在的表达式的计算结果(二者都是内存中的数据,只是内存中存在的生命期不同),比如:
int x = 5 + 6; // x是左值,其数据在作用域内一直存在。5 + 6的计算结果则是右值
double y = sin(x); // y是左值。sin(x)的计算结果是右值,该语句执行后,右值不再存在
也就是说,右值所使用的是临时内存空间,即:右值是一个临时变量或临时对象,当表达式或函数返回值赋值给左值后,该临时数据即被系统销毁。这无疑是一种浪费(多了一次分配和销毁内存的操作),也影响了性能。右值引用的目的是在表达式计算完毕后,可以继续使用这块临时内存中的数据(计算结果),减少一次内存分配与销毁。其内部原理是使用了转移语义,即:将右值数据“转移”给右值引用对象。
使用常规的左值引用的语法(&)无法引用右值,必须使用“&&”。
4、移动拷贝构造函数和移动赋值运算符重载
class MyCalss
{
public:
Myclass() { } // 构造函数
MyClass(const MyClass&) { } // 拷贝构造函数
MyClass(const MyClass&&) { } // 移动拷贝构造函数
MyClass& operator= (const MyClass&) { } // 赋值运算符重载
MyClass& operator= (const MyClass&&) { } // 移动赋值运算符重载
~MyClass() { } // 析构函数
}
移动拷构函数和移动赋值重载与传统的同名函数基本一致,不同处:其参数类型采用了右值引用语法(两个&&)。定义类的时候,如果声明了传统的拷构函数和赋值重载,则编译器不再自动生成移动拷构和移动赋值,同理,如果声明了移动拷构和移动赋值,则编译器也不再自动生成传统的拷构和赋值。如果既没有声明传统的拷构和赋值,也没有声明移动拷构和移动赋值,则编译器会自动生成这4个函数。
移动拷贝构造函数的目的与用途:当使用临时对象来构造一个新对象时,传统的左值引用,将为临时对象分配内存,用完后还要销毁,而采用右值引用的转移语义,无需对临时对象进行内存分配与销毁,提高对象的构造性能。
5、六大函数的禁用
六大函数指的是:构造函数、拷构函数、移动拷构、赋值重载、移动赋值重载、析构函数。传统上,要禁用这6大函数中的某一个或多个,需将其声明在private区中,C++ 11扩展了关键字delete的语义,将该关键字放在函数声明的最后,可起到同样的效果(这种语法格式,类似于声明纯虚函数):
MyClass(const MyClass&) = delete; // 禁用拷构函数
MyClass& operator= (const MyClass&) = delete; // 禁用赋值运算符重载函数
6、Lambda表达式(就地展开式匿名函数)
Lambada表达式可取代函数参数中的函数指针和函数对象,并且,它相当于一个匿名函数,该函数的声明与语句同在一起,这样,可以少写一个函数或类(如果使用函数对象技术),直接给出函数语句即可。见示例:
// 3参func是函数bool func()的指针
int value = count_if (v.begin(), v.end(), func);
// 3参是函数对象,即重载了运算符operator()的类functor,operator()的返回类型为bool
int value = count_if (v.begin(), v.end(), functor(5));
// 3参是Lambda表达式(就地展开式匿名函数)
int value = count_if (v.begin(), v.end(), [](int x) {return x % 5 == 0});
使用函数指针,代码不可内联,而函数对象和Lambda均可。并且,Lambda可访问作用域内的任何变量,只需将该变量写在[]内即可(可按值传递,也可按引用传递)。
7、智能指针:作用域指针unique_ptr<T>和引用计数指针shared_ptr<T>
这两个智能指针都是类模板,隶属的头文件是<memory>,其概念与用法与JUCE类库的ScopedPointer作用域指针类模板和ReferenceCountedObject引用计数类基本一致。不同之处:
 C++ 11标准库新增的unique_ptr智能指针可以指向位于堆中的数组,比如:
unique_ptr<float[]> myArray = new float[arraySize];
unique_ptr<float[]> myArray (new float[arraySize]); // 小括号初始化语法
float* myArray = new float[arraySize]; // 传统的声明数组语法
 shared_ptr则可以实现多个指针共享同一个堆对象,当最后一个指针也不再“指向”该对象时,自动销毁该对象并释放堆内存,用法同上。shared_ptr的作用与JUCE的引用计数类ReferenceCountedObject基本一致,但省略了派生子类和特殊的声明语法。JUCE类库中,某个类要成为引用计数类,必须继承自ReferenceCountedObject类(直接继承即可,无需实现任何虚函数,除析构函数外,该类也没有虚函数),声明和初始化引用计数类指针变量的语法为:
ReferenceCountedObjectPtr<MyClass> ptr = new MyClass();
shared_ptr<T>与unique_ptr<T>的另一个区别是:shared_ptr不能指向堆中的数组,而unique_ptr可以指向堆数组。声明并初始化shared_ptr指针的语法有三,一是常规赋值方式,二是常规构造方式,三是使用auto关键字和make_shared()函数。推荐使用第三种方式。见下:
// 常规赋值方式声明并初始化引用计数指针
shared_ptr<MyClass> ptr = new MyClass();
// 常规构造方式声明并初始化引用计数指针
shared_ptr<MyClass> ptr (new MyClass());
// 使用auto关键字和make_shared()函数
auto ptr = make_shared<MyClass>();
 不同于JUCE类库的作用域指针和引用计数指针,C++ 11新增的shared_ptr和unique_ptr可作为函数的返回值,即:函数可以返回智能指针所指向的堆对象,而非传统的普通指针所指向的堆对象,并且返回值赋值给所声明的智能指针时,自动使用移动语义,非常高效。JUCE类库的智能指针则不可作为函数的返回值。
 此外,unique_ptr不支持传统的赋值和拷贝构造,不能将某个unique_ptr指针直接赋值给另一个,但可以通过移动语义来实现(赋值给新指针后,原指针自动置为nullptr):
unique_ptr<MyClass> p1(new MyClass());
unique_ptr<MyClass> p2 = p1; // 错误!无法编译。不能直接赋值给另一个
// OK!使用移动语义进行“转移式”赋值
unique_ptr<MyClass> p3 = std::move(p1);
p1转移式赋值给p3后,p1自动变为nullptr。
 优先使用shared_ptr,而不是优先使用unique_ptr。
8、final关键字(禁止派生子类或重写成员函数)和override关键字(此为重写的父类虚函数)
声明类时,最后加final关键字,意味着该类不可派生子类。在基类的某个成员函数的声明语句最后添加final,则意味着该函数不可被子类重写:
class MyClass final // 该类不可派生子类
{
public:
// 该函数不可被子类重写(此处仅是演示。因为本类已被声明为不可派生子类)
void doSomething() final;
// …
};

class BaseClass;
class subClass : public BaseClass
{
/** 该函数是重写的父类同名虚函数,而不是新增的函数。如果与父类的此函数的原型不一致,则编译器报错。此关键字可防止:子类重写父类虚函数后,父类又修改了虚函数的函数原型(比如参数类型),从而导致子类原本重写的虚函数成为新增的函数并隐藏父类的同名函数 */
virtual void mustOverride() override;
};
9、auto用于函数返回类型:
函数的返回类型放在“->”之后,而原来放置返回类型的最开始处,用auto关键字替代。
auto func(const int i) -> int
{
return i + 2;
}
该语法对于普通函数的用处不大,但用于模板函数的返回类型时非常有用。