【C++】C++11 重要新特性

C++11 重要新特性

1. range-based for 语句

用于更方便地遍历容器(例如数组、vector、list 等)中的元素。

语法格式:

for (range_declaration : range_expression) {
    // 循环体
}

range_declaration 是一个变量,用于接收容器中的每个元素。
range_expression 是要遍历的容器,可以是数组、STL 容器或其他支持 begin() 和 end() 函数的对象。

示例:

std::vector<int> nums = {1, 2, 3, 4, 5};
for (const auto& num : nums) {
    std::cout << num << " ";
}

2. nullptr

nullptr_t 是 C++11 标准引入的一个新类型,专门用于表示 nullptr。它是一种空指针常量的类型,是为了解决在类型推导过程中可能产生的二义性问题。

表示空指针,nullptr 是一个属于 nullptr_t 类型的字面常量,不同于 NULL 或 0,它没有整数类型。nullptr 被用来初始化指针、作为函数参数传递,并在条件判断语句中进行比较。使用 nullptr 有助于编写更安全、更明确的代码。

int* ptr1 = nullptr;         // 使用 nullptr 初始化指针
int* ptr2 = 0;              // 传统方式,但不推荐

3. auto 类型指示符

使用 auto 声明的变量可以根据其初始化表达式的类型来确定其类型,从而简化代码书写。

语法:

auto variable = expression; 
//其中 expression 是变量的初始化表达式。编译器会根据初始化表达式的类型推导出变量的类型。

需要注意的是,auto 并不是一种动态类型,而是在编译期确定的静态类型

4. decltype 类型指示符

它的作用是返回操作数的数据类型。编译器分析表达式并得到它的类型,但不实际计算表达式的值。

decltype(f()) sum = x;  //sum的类型就是函数 f 返回类型

编译器并不实际调用函数f ,只是返回类型作为sum的类型。

切记: decltype((variable)) (注意是双层括号) 的结果永远是引用,而 decltype(variable) 结果只有当 variable 本身就是一个引用时才是引用。

1. 基本用法:

    decltype(x + 1) z = 3;  // z的类型为int,因为x + 1的类型为int
    int a = 3, b = 4;
    decltype(a) c = a;
    decltype((b)) d = a;
    cout << ++c << " " << ++d << endl; // 4 4

2. 结合模板:

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {   
//这是一种使用尾置返回类型(trailing return type)语法的写法,主要用于指定函数返回类型。在这个写法中,返回类型写在 -> 后面。通常结合auto关键字使用。
    return t + u;    
}
    
int main() {  
	int a = 5;    
	double b = 3.14;
    auto result = add(a, b);  // result的类型为double,decltype(a + b)为double
    return 0; 
} 

3. decltype(auto):

int x = 42;
// 使用decltype(auto)保留引用类型,因为 x 是一个 int 类型的变量,而 decltype(auto) 也会保留 x 的引用类型。
decltype(auto) y = x;  // y的类型为int&,是x的引用

5. unifrom initialization

是 C++11 引入的一种初始化语法,它的目标是提供一种统一的初始化方式,适用于各种类型的对象。

// 直接初始化    
int a{42};
 // 拷贝初始化    
 double b = {3.14};
// 列表初始化数组    
int arr[]{1, 2, 3, 4, 5};
// 列表初始化容器    
std::vector<int> vec{1, 2, 3, 4, 5};

6. initializer_list

std::initializer_list 是 C++11 引入的一种新特性,它允许你使用花括号 {} 语法初始化对象和数组。

initializer_list 是一个轻量级的容器,它可以包含任何类型的值。你可以将 initializer_list 作为函数或构造函数的参数,以便接受花括号初始化列表。

#include<initializer_list>

class MyClass {
    vector<int> data;
public:
    MyClass(initializer_list<int> init) : data(init) {} 
};

MyClass obj {1, 2, 3, 4, 5}; // 使用花括号初始化列表初始化 MyClass 对象

以下情况下使用:

  1. 当你在自己的类或函数中直接使用 initializer_list 类型时。例如,如果你想让你的类的构造函数接受一个初始化列表,或者你想在你的函数中处理一个初始化列表,你就需要包含 <initializer_list> 头文件。
MyClass(initializer_list<int> list) {// 处理初始化列表}
  1. 当你需要访问 initializer_list 类型的成员函数(如 begin()、end()、size())时。虽然你可以在不包含 <initializer_list> 头文件的情况下创建和传递初始化列表,但如果你需要访问这些函数,你就需要包含这个头文件。
#include <initializer_list>
void printSize(initializer_list<int> list) {
    cout << "Size: " << list.size() << endl; 
}

7. explicit(不是新特性)

explicit 关键字主要用于修饰单参数的构造函数,目的是防止隐式类型转换。

// 单参数构造函数声明为 explicit
    explicit MyClass(int x) : value(x) {}
// 多参数构造函数
    MyClass(int x, int y) : value(x + y) {}  // 没有使用explicit

多参数构造函数通常不会导致隐式类型转换问题,因为需要提供足够的参数,这使得类型转换更为显式。

8. 可变参数模板(Variadic templates)

它允许你创建接受任意数量参数的模板函数或模板类。在此之前,模板的参数数量是固定的,但引入可变参数模板后,可以编写接受任意数量参数的模板。

可变参数模板主要使用在函数模板和类模板中,通过 … 语法来表示可变数量的参数。这种特性使得编写更加灵活的模板成为可能。允许处理不定数量的参数类型。

(1) 函数模板中的可变参数模板:

#include <iostream>

// 基本情况:递归终止
template <typename T>
void print(const T& value) {
    std::cout << value << std::endl;
}

// 递归情况:打印第一个参数并递归调用打印其余参数
template <typename T, typename... Args>
void print(const T& first, const Args&... args) {
    std::cout << first << ", ";
    print(args...);
}

int main() {
    print(1, 2.5, "Hello", 'c');
    return 0;
}

在上面的示例中,print函数模板通过可变参数模板实现了接受任意数量参数的功能。递归地调用print函数,每次打印第一个参数,并递归调用以打印其余参数。

(2) 类模板中的可变参数模板:

#include <iostream>

// 类模板的基本情况:递归终止
template <typename T>
class Tuple {
public:
    Tuple(const T& value) : data(value) {}

    void print() const {
        std::cout << data << std::endl;
    }

private:
    T data;
};

// 类模板的递归情况:包含一个元素和递归包含其余元素
template <typename T, typename... Args>
class Tuple {
public:
    Tuple(const T& value, const Args&... args) : data(value), rest(args...) {}

    void print() const {
        std::cout << data << ", ";
        rest.print();
    }

private:
    T data;
    Tuple<Args...> rest;
};

int main() {
    Tuple<int, double, std::string> myTuple(1, 2.5, "Hello");
    myTuple.print();

    return 0;
}

在上面的示例中,Tuple类模板使用可变参数模板实现了接受任意数量的参数。在递归情况下,每个实例都包含一个元素和递归的Tuple实例,这样就可以处理不同数量的模板参数。

9. default

如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数。

default 关键字可以用于显式地要求编译器生成默认的版本。也可用于默认拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。

MyClass() = default;

10. delete

主要用于删除特殊的成员函数,如拷贝构造函数和拷贝赋值运算符,以防止编译器自动生成它们。

MyClass(const MyClass&) = delete; // 禁止拷贝构造函数
MyClass& operator =(const MyClass&) = delete; // 禁止拷贝赋值运算符

11. alias templates(模板别名)

模板别名允许给现有的模板起一个新的名字,使代码更简洁易读。

语法:

template <typename T>
using NewName = OldTemplate<T>;

示例:

template <typename T>
using MyContainer = std::vector<T>;   //MyContainer 是 std::vector 的别名
MyContainer<int> numbers = {1, 2, 3, 4, 5};

12. Template Template Parameter(模板模板参数)

允许你将模板作为另一个模板的参数。模板参数的推导并不会自动考虑默认模板参数。如果你传递的模板有默认参数,你仍然需要显式提供所有参数。

语法:

template <template <typename> class TemplateParameter>
class MyClass {
    // 使用 TemplateParameter 进行操作
};

示例:

#include <iostream>

// 声明一个模板模板参数
template <template <typename> class Container>
class Wrapper {
public:
    // 使用模板模板参数作为成员
    Container<int> container;

    // 构造函数初始化
    Wrapper(const Container<int>& initialContainer) : container(initialContainer) {}

    // 打印容器元素
    void print() {
        for (const auto& element : container) {
            std::cout << element << " ";
        }
        std::cout << std::endl;
    }
};

// 定义一个模板类作为模板模板参数
template <typename T>
class MyContainer {
public:
    T value;

    MyContainer(const T& val) : value(val) {}

    // 添加打印功能
    void print() const {
        std::cout << value << std::endl;
    }
};

int main() {
    // 使用 Wrapper包装 MyContainer
    Wrapper<MyContainer> wrapper(MyContainer<int>(42));

    // 打印 Wrapper中的容器元素
    wrapper.print();

    return 0;
}

在这个示例中,Wrapper是一个使用模板模板参数的类模板,它接受一个模板类 Container 作为参数。MyContainer 是一个简单的模板类,包含一个值,并具有打印功能。在 main 函数中,我们创建了一个 Wrapper 对象,将 MyContainer
作为模板参数传递给 Wrapper。最后,我们调用 Wrapper 的 print 函数来打印容器元素。

13. 类型别名(Type Alias)

用来为现有类型定义一个新的名称。它可以提高代码的可读性和可维护性。在C++中,有两种方式定义类型别名:使用关键字typedef和使用关键字using。

在现代 C++ 中,using 更为推荐,因为它更灵活、语法更清晰。

示例:

使用 using 定义类型别名:
    using Integer = int;  // 将 int 类型定义为 Integer
    using DoubleVector = std::vector;  // 将 std::vector 定义为 DoubleVector
    using SizeType = std::size_t;  // 使用标准库定义的 std::size_t 作为类型别名
    using Predicate = bool (*)(int);  // 使用函数指针作为类型别名
    
    template <typename T>
    using MyVector = std::vector;  // 使用模板别名

14. noexcept

当函数被声明为 noexcept 时,它表示该函数不会抛出异常。这有助于编译器进行一些优化,并使程序员能够更清晰地了解函数的异常行为。

语法:

  1. 函数声明中使用 noexcept 关键字的语法如下:
void myFunction() noexcept {
    // 函数体
}
  1. 或者可以指定一个条件,如果该条件为 true,则函数不会抛出异常:
void myFunction() noexcept(condition) {
    // 函数体
}

示例:

#include <iostream>

// 带有异常的函数
int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero");
    }
    return a / b;
}

// 带有 noexcept 的函数
int safe_divide(int a, int b) noexcept {
    if (b == 0) {
        // 在 noexcept 函数中抛出异常,会导致程序终止
        throw std::runtime_error("Division by zero");
    }
    return a / b;
}

int main() {
    try {
        // 调用带有异常的函数
        int result = divide(10, 2);
        std::cout << "Result: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }

    try {
        // 调用带有 noexcept 的函数
        int result = safe_divide(10, 0);
        std::cout << "Result: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

在这个示例中,divide 函数可能抛出异常,而 safe_divide 函数使用 noexcept 表示它不会抛出异常。在 main 函数中,我们分别调用这两个函数,并使用 try 和 catch 块捕获异常。请注意,如果在 noexcept 函数中抛出异常,会导致程序终止。

15. override

override 关键字用于在派生类中显式指定对基类虚函数的覆盖。它有助于在编译时检测错误,例如拼写错误或者意外不匹配的函数签名。
如果在派生类中使用 override 关键字声明一个函数,但该函数并没有覆盖基类中的虚函数,编译器将生成一个错误。

#include <iostream>

class Base {
public:
    virtual void print() const {
        std::cout << "Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    // 使用 override 关键字表明该函数覆盖了基类的虚函数
    void print() const override {
        std::cout << "Derived class" << std::endl;
    }
    // 拼写错误 编译时检测错误
    //void prin() const override {
    //    std::cout << "Derived class" << std::endl;
    //}
};

16. final

final 关键字用于在基类中阻止某个虚函数被进一步覆盖,或者在派生类中阻止某个类被继续派生。
在基类中使用 final 关键字,可以防止派生类再次覆盖该虚函数。

  1. 禁止继承的类示例:
class FinalClass final {
public:
    // 类声明中使用 final,表示该类不能被继承
    void someFunction() const {
        // 类的成员函数实现
    }
};

// 下面的派生类声明是非法的,会导致编译错误
// class Derived : public FinalClass {};

  1. 禁止覆盖的虚函数示例:
class Base {
public:
    // 使用 final 关键字表示该虚函数不能被派生类覆盖
    virtual void someFunction() const final {
        // 虚函数的实现
    }
};

class Derived : public Base {
    // 下面的函数声明是非法的,会导致编译错误
    // void someFunction() const override;
};

17. 一元谓词、二元谓词以及仿函数

(1)一元谓词(Unary Predicate):

一元谓词是一个接受一个参数的函数或函数对象
它通常用于表示某种条件,例如判断元素是否满足某个条件。
一元谓词通常用于像 find_if、remove_if 等算法中,这些算法需要一个判断条件。

// 一元谓词的示例
bool isEven(int x) {
    return x % 2 == 0;
}

(2)二元谓词(Binary Predicate):

二元谓词是一个接受两个参数的函数或函数对象
它通常用于表示某种操作或比较两个元素。
二元谓词通常用于像 sort、max_element 等需要比较或操作两个元素的算法中。

// 二元谓词的示例
bool compare(int a, int b) {
    return a < b;
}

(3)仿函数(函数对象):

仿函数是一个类或对象,它可以像函数一样被调用。
它可以重载 operator(),使得对象的行为类似于函数调用。
仿函数通常用于需要封装某种操作或条件的算法中,例如像 transform 中的转换操作。

// 仿函数的示例
struct Multiply {
    int operator()(int a, int b) const {
        return a * b;
    }
};

18. lambda

一个lambda表达式表示一个可调用的代码单元。可以理解为一个未命名的内联函数。一个lamba具有一个返回类型,一个参数列表和一个函数体。但与函数不同,lambdas可能定义再函数内部。

lambda表达式具有如下形式:

[capture list](parameter list) -> return type { function body }

capture list(捕获列表)是一个lambda 所在函数中定义的局部变量的列表(通常为空);
return type , parameter list, function body与普通函数一样,分别表示返回类型,参数列表和函数体;
与普通函数不同,lambda必须使用尾置返回类型。即 -> return type ,可以使用decltype关键字来返回类型。

示例:

//空捕获列表表示此lambda 不使用它所在的函数中的任何局部变量。lambda 函数体比较两个参数的size() ,并根据两者的相对大小返回一个布尔值。
[ ](const string &a,const string &b){ 
	return a.size() < b.size(); 
}

lambda 表达式可以当参数传入算法和函数当中。

// Lambda 表达式作为参数传递给 for_each 算法    
for_each(numbers.begin(), numbers.end(), [](int x){cout << x << " ";});
//lambda表达式定义了一个匿名函数,用于输出容器中的每个元素。

19. 右值引用

右值引用就是必须绑定到右值的引用,通过&& 而不是 & 来获得右值引用。显然右值引用有一个重要的性质,只能绑定到一个将要销毁的对象。因此我们可以将一个右值引用的资源赋值到另一个对象中。

左值和右值的区别:

左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中的临时对象。

不能将一个右值引用绑定到一个右值引用类型的变量上:

int &&rr1 = 42; // 正确;字面量是右值
int &&rr2 = rr1; // 错误;表达式 rr1 是左值!

本文到此结束
欢迎关注🔎点赞👍收藏⭐️留言📝

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值