2023-4-6-C++11、C++14、C++17、C++20版本新特性系统全面的学习!(二)

简介: 2023-4-6-C++11、C++14、C++17、C++20版本新特性系统全面的学习!

智能指针

c++11引入了三种智能指针:

  • std::shared_ptr
  • std::weak_ptr
  • std::unique_ptr

shared_ptr

shared_ptr使用了引用计数,每一个shared_ptr的拷贝都指向相同的内存,每次拷贝都会触发引用计数+1,每次生命周期结束析构的时候引用计数-1,在最后一个shared_ptr析构的时候,内存才会释放。

智能指针还可以自定义删除器,在引用计数为0的时候自动调用删除器来释放对象的内存,代码如下:

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

weak_ptr

weak_ptr是用来监视shared_ptr的生命周期,它不管理shared_ptr内部的指针,它的拷贝的析构都不会影响引用计数,纯粹是作为一个旁观者监视shared_ptr中管理的资源是否存在,可以用来返回this指针和解决循环引用问题。

unique_ptr

std::unique_ptr是一个独占型的智能指针,它不允许其它智能指针共享其内部指针,也不允许unique_ptr的拷贝和赋值。使用方法和shared_ptr类似,区别是不可以拷贝,unique_ptr也可以像shared_ptr一样自定义删除器,使用方法和shared_ptr相同。

#include <functional>
#include <iostream>
#include "chrono"
using namespace std;
class A {
public:
    A() {};
    ~A() {
        std::cout << "111" << std::endl;
    }
};
int main() {
    std::shared_ptr<A> sharedPtr = std::make_shared<A>();
    std::unique_ptr<A> uniquePtr = std::unique_ptr<A>(new A());
    std::shared_ptr<A> sharedPtr1(new A(), [](A *a) {
        std::cout << "wo" << std::endl;
    });//自定义删除器
    //输出wo 111 111 后构造的先析构
    return 0;
}

基于范围的for循环

vector<int> vec;
for (auto iter = vec.begin(); iter != vec.end(); iter++) { // before c++11
    cout << *iter << endl;
}
for (int i : vec) { // c++11基于范围的for循环
    cout << "i" << endl;
}

委托构造函数

委托构造函数允许在同一个类中一个构造函数调用另外一个构造函数,可以在变量初始化时简化操作,通过代码来感受下委托构造函数的妙处吧:

struct A {
    A(){}
    A(int a) { a_ = a; }
    A(int a, int b) : A(a) { b_ = b; }
    A(int a, int b, int c) : A(a, b) { c_ = c; }
    int a_;
    int b_;
    int c_;
};

继承构造函数

继承构造函数可以让派生类直接使用基类的构造函数,如果有一个派生类,我们希望派生类采用和基类一样的构造方式,可以直接使用基类的构造函数,而不是再重新写一遍构造函数,老规矩,看代码:

不使用继承构造函数:

struct Base {
    Base() {}
    Base(int a) { a_ = a; }
    Base(int a, int b) : Base(a) { b_ = b; }
    Base(int a, int b, int c) : Base(a, b) { c_ = c; }
    int a_;
    int b_;
    int c_;
};
struct Derived : Base {
    Derived() {}
    Derived(int a) : Base(a) {} // 好麻烦
    Derived(int a, int b) : Base(a, b) {} // 好麻烦
    Derived(int a, int b, int c) : Base(a, b, c) {} // 好麻烦
};
int main() {
    Derived a(1, 2, 3);
    return 0;
}

使用继承构造函数:

struct Base {
    Base() {}
    Base(int a) { a_ = a; }
    Base(int a, int b) : Base(a) { b_ = b; }
    Base(int a, int b, int c) : Base(a, b) { c_ = c; }
    int a_;
    int b_;
    int c_;
};
struct Derived : Base {
    using Base::Base;
};
int main() {
    Derived a(1, 2, 3);
    return 0;
}

只需要使用using Base::Base继承构造函数,就免去了很多重写代码的麻烦。

nullptr

nullptr是c++11用来表示空指针新引入的常量值,在c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL本质上是个int型的0,其实不是个指针。举例:

void func(void *ptr) {
    cout << "func ptr" << endl;
}
void func(int i) {
    cout << "func i" << endl;
}
int main() {
    func(NULL); // 编译失败,会产生二义性
    func(nullptr); // 输出func ptr
    return 0;
}

final

final用于修饰一个类,表示禁止该类进一步派生和虚函数的进一步重载。

struct Base final {
    virtual void func() {
        cout << "base" << endl;
    }
};
struct Derived : public Base{ // 编译失败,final修饰的类不可以被继承
    void func() override {
        cout << "derived" << endl;
    }
};

override

override用于修饰派生类中的成员函数,标明该函数重写了基类函数,如果一个函数声明了override但父类却没有这个虚函数,编译报错,使用override关键字可以避免开发者在重写基类函数时无意产生的错误。

struct Base {
    virtual void func() {
        cout << "base" << endl;
    }
};
struct Derived : public Base{
    void func() override { // 确保func被重写
        cout << "derived" << endl;
    }
    void fu() override { // error,基类没有fu(),不可以被重写
    }
};

default

c++11引入default特性,多数时候用于声明构造函数为默认构造函数,如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数,如下代码:

struct A {
    int a;
    A(int i) { a = i; }
};
int main() {
    A a; // 编译出错
    return 0;
}

上面代码编译出错,因为没有匹配的构造函数,因为编译器没有生成默认构造函数,而通过default,程序员只需在函数声明后加上“=default;”,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体,如下:

struct A {
    A() = default;
    int a;
    A(int i) { a = i; }
};
int main() {
    A a;
    return 0;
}

编译通过。

delete

c++中,如果开发人员没有定义特殊成员函数,那么编译器在需要特殊成员函数时候会隐式自动生成一个默认的特殊成员函数,例如拷贝构造函数或者拷贝赋值操作符,如下代码:

struct A {
    A() = default;
    int a;
    A(int i) { a = i; }
};
int main() {
    A a1;
    A a2 = a1;  // 正确,调用编译器隐式生成的默认拷贝构造函数
    A a3;
    a3 = a1;  // 正确,调用编译器隐式生成的默认拷贝赋值操作符
}

而我们有时候想禁止对象的拷贝与赋值,可以使用delete修饰,如下:

struct A {
A() = default;
A(const A&) = delete;
A& operator=(const A&) = delete;
int a;
A(int i) { a = i; }
};
int main() {
A a1;
A a2 = a1; // 错误,拷贝构造函数被禁用
A a3;
a3 = a1; // 错误,拷贝赋值操作符被禁用
}

delele函数在c++11中很常用,std::unique_ptr就是通过delete修饰来禁止对象的拷贝的。

explicit

explicit专用于修饰构造函数,表示只能显式构造,不可以被隐式转换,根据代码看explicit的作用:

#include <iostream>
using namespace std;
class Point {
public:
    int x, y;
    Point(int x = 0, int y = 0)
            : x(x), y(y) {}
};
void displayPoint(const Point& p)
{
    cout << "(" << p.x << ","
         << p.y << ")" << endl;
}
int main()
{
    displayPoint(1);//输出(1,0),y使用了默认值0
    Point p = 1;
}

我们定义了一个再简单不过的Point类, 它的构造函数使用了默认参数. 这时主函数里的两句话都会触发该构造函数的隐式调用. (如果构造函数不使用默认参数, 会在编译时报错)

显然, 函数displayPoint需要的是Point类型的参数, 而我们传入的是一个int, 这个程序却能成功运行, 就是因为这隐式调用. 另外说一句, 在对象刚刚定义时, 即使你使用的是赋值操作符=, 也是会调用构造函数, 而不是重载的operator=运算符.

这样悄悄发生的事情, 有时可以带来便利, 而有时却会带来意想不到的后果. explicit关键字用来避免这样的情况发生。

// 加了explicit之后的代码
#include <iostream>
using namespace std;
class Point {
public:
    int x, y;
    explicit Point(int x = 0, int y = 0)
            : x(x), y(y) {}
};
void displayPoint(const Point& p)
{
    cout << "(" << p.x << ","
         << p.y << ")" << endl;
}
int main()
{
    displayPoint(Point(1));
    Point p(1);
}

const

const字面意思为只读,可用于定义变量,表示变量是只读的,不可以更改,如果更改,编译期间就会报错。

主要用法如下:

  • 用于定义常量,const的修饰的变量不可更改。
const int value = 5;
  • 指针也可以使用const,这里有个小技巧,从右向左读,即可知道const究竟修饰的是指针还是指针所指向的内容。“左定值,右定向,const修饰不变量”。
char *const ptr; // 指针本身是常量
const char* ptr; // 指针指向的变量为常量
  • 在函数参数中使用const,一般会传递类对象时会传递一个const的引用或者指针,这样可以避免对象的拷贝,也可以防止对象被修改。
class A{};
void func(const A& a);
  • const修饰类的成员变量,表示是成员常量,不能被修改,可以在初始化列表中被赋值。
class A {
    const int value = 5;
};
class B {
    const int value;
    B(int v) : value(v){}
};
  • 修饰类成员函数,表示在该函数内不可以修改该类的成员变量。
class A{
    void func() const;
};
  • 修饰类对象,类对象只能调用该对象的const成员函数。
class A {
    void func() const;
};
const A a;
a.func();
  • const参数传递和函数返回值
  • const参数传递和函数返回值

constexpr

constexpr是c++11新引入的关键字,用于编译时的常量和常量函数,这里直接介绍constexpr和const的区别:

两者都代表可读,const只表示read only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量,而constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量,但是如果编译期间此函数不能被计算出来,那它就会当作一个普通函数被处理。如下代码:

#include<iostream>
using namespace std;
constexpr int func(int i) {
    return i + 1;
}
int main() {
    int i = 2;
    func(i);// 普通函数
    func(2);// 编译期间就会被计算出来
}

enum class

c++11新增有作用域的枚举类型

不带作用域的枚举代码:

enum AColor {
    kRed,
    kGreen,
    kBlue
};
enum BColor {
    kWhite,
    kBlack,
    kYellow
};
int main() {
    if (kRed == kWhite) {
        cout << "red == white" << endl;
    }
    return 0;
}

如上代码,不带作用域的枚举类型可以自动转换成整形,且不同的枚举可以相互比较,代码中的红色居然可以和白色比较,这都是潜在的难以调试的bug,而这种完全可以通过有作用域的枚举来规避。

有作用域的枚举代码:

enum class AColor {
    kRed,
    kGreen,
    kBlue
};
enum class BColor {
    kWhite,
    kBlack,
    kYellow
};
int main() {
    if (AColor::kRed == BColor::kWhite) { // 编译失败
        cout << "red == white" << endl;
    }
    return 0;
}

使用带有作用域的枚举类型后,对不同的枚举进行比较会导致编译失败,消除潜在bug,同时带作用域的枚举类型可以选择底层类型,默认是int,可以改成char等别的类型。

enum class AColor : char {
    kRed,
    kGreen,
    kBlue
};

我们平时编程过程中使用枚举,一定要使用有作用域的枚举取代传统的枚举。

非受限联合体

c++11之前union中数据成员的类型不允许有非POD类型,而这个限制在c++11被取消,允许数据成员类型有非POD类型,看代码:

struct A {
    int a;
    int *b;
};
union U {
    A a; // 非POD类型 c++11之前不可以这样定义联合体
    int b;
};
union Myunion{
    int a = 0 ;
    double b = 0.0;//报错  因为在union联合体中只能对一个变量进行定义
    char c ='a';//报错  同理
};

😆小知识

在这里顺便补充一下内存对齐的知识:

union(联合体)类型中的数据共用内存,联合的所有成员共用一段内存空间,存储地址的起始位置都相同,一般来说最大成员的内存宽度作为union的内存大小,还必须满足是所有成员的整数倍,主要的原因是为了节省内存空间,默认的访问权限是公有的,但是它同样要遵守内存对齐的原则。

一,内存对齐的三条规则

  • 数据成员对齐规则,结构体(struct)(或联合(union))的数据成员,第一个数据成员存放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员(只要该成员有子成员,比如数组、结构体等)大小的整数倍开始(如:int 在 64bit 目标平台下占用 4Byte,则要从4的整数倍地址开始存储)
  • 结构体作为成员,如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储
  • 结构体的总大小,即sizeof的结果,必须是其内部最大成员长度(即前面内存对齐指令中提到的有效值)的整数倍,不足的要补齐

二,注意事项

  • 数组在内存中存储时是分开存储的,char类型的数组每个元素是 1Byte,内存对齐时按照单个元素进行对齐
  • C++中空结构体占用 1Byte
  • C++中空类占用 1Byte
  • 类和结构体一样,需要内存对齐
union Myunion {
    int a = 0;
    double b;
    char c;
};
struct Mystruct {
    int a = 0;
    double b;
    char c;
};
int main() {
    Myunion myunion;
    Mystruct mystruct;
    myunion.b = 2.0;
    std::cout << myunion.a << std::endl;//0
    std::cout << myunion.b << std::endl;//2
    std::cout << myunion.c << std::endl;//
    std::cout << "a " << &myunion.a << std::endl;//a 00000049666FF6B8
    std::cout << "b " << &myunion.b << std::endl;//b 00000049666FF6B8
    std::cout << "c " << &myunion.c << std::endl;//c
    std::cout << sizeof(myunion) << std::endl;//8  原因是因为内部占用空间最大的元素是double类型,占用八个字节
    std::cout << sizeof(mystruct) << std::endl;//24 原因是因为内部占用空间最大的元素是double类型,占用八个字节,一共三个元素,共3*8
    return 0;
}

注意下面这种情况,内存大小为48

struct Test {
    int a;
    double b;
    char c;
};
struct Test3 {
    int a;
    Test d;
    double b;
    char c;
};//48

😆小知识

字节对齐的原因

  • 平台原因(移植原因),不是所有的硬件平台都能任意访问地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
  • 性能原因,提高CPU内存访问速度,一般处理器的内存存取粒度都是N的整数倍,假如访问N大小的数据,没有进行内存对齐,有可能就需要两次访问才可以读取出数据,而进行内存对齐可以一次性把数据全部读取出来,提高效率。

sizeof

不需要定义一个对象,在计算对象的成员大小。

struct Mystruct {
    int a = 0;
    double b;
    char c;
};
int main() {
    //C++11之前只能通过Mystruct struct1; sizeof(struct1.b),现在可以通过下面这个方式使用sizeof
    std::cout << sizeof(Mystruct::b) << std::endl;//8
    return 0;
}

assertion

通常来说,断言并不是正常程序所必需的,但对于程序调试来说,通常断言能够帮助开发者快速定位那些违反了某些前提条件的程序错误。在C++中,头文件提供了assert宏,提供运行时断言。如下:

#include <cassert>
#include "iostream"
int main() {
    int a = 5;
    int b = 0;
    assert(b != 0);
    std::cout << a / b << std::endl;
    return 0;
}

上述代码中对除数使用了断言,当除数为0时程序会报错。

然而,前述的断言只能在运行时才会起作用,这意味着不运行程序我们不会知道程序是否有错,而且就算运行了也只有在的调用到assert相关的代码路径时才会检查出来,这再某些情况下是不可接受的。因此我们还需要(特别是在模板编程中)在编译时期就能产生断言的机制。为此,C++11推出了静态断言。

C++的静态断言语法很简单,也可以自定义错误提示信息:

static_assert(bool_constexpr, message) 从C++11起

使用静态断言,我们可以在编译期间发现更多的错误,用编译器来强制保证一些契约

断言(assertion)是编程中的一种常用手段,在通常情况下,断言就是将一个返回值总是真(或者我们需要是真)的判别式放在语句中,用以排除在设计逻辑上不应该出现的情况。举个例子:我们都知道除数不能为0,那么就可以对除数使用断言,以使程序在除数为0的情况下产生异常退出。

#include <cassert>
#include "iostream"
int main() {
    int a = 5;
    int b = 0;
    static_assert(b != 0);//编译的时候就提示错误,这里会显示红色的下波浪线
    std::cout << a / b << std::endl;
    return 0;
}

自定义字面量

C++11允许用户自定义字面量后缀

#include <cassert>
#include "iostream"
#include "thread"
constexpr long double operator "" _cm(long double x) {
    return x * 10;
}
constexpr long double operator "" _m(long double x) {
    return x * 1000;
}
constexpr int operator "" _mm(unsigned long long x) {
    return (int) x;
}
int main() {
    std::this_thread::sleep_for(std::chrono::microseconds(1000_mm));
    return 0;
}

如果希望在编译时就调用字面量后缀函数,则需要把函数定义为 constexpr

内存对齐

内存对齐的知识可以看上面非受限联合体里面的讲解,这里对上述的讲解进行补充扩展

std::aligned_storage可以看成一个内存对其的缓冲区

sizeof : 获取内存存储的大小。

alignof : 获取地址对其的大小,POD里面最大的内存对其的大小。

#include <cassert>
#include "iostream"
#include "thread"
class A {
    int a;
    char b;
};
int main() {
    //下面这个函数用于创建一块对齐的内存,
    static std::aligned_storage<sizeof(A),
            alignof(A)>::type data;
    std::cout << typeid(data).name() << std::endl;//union std::_Align_type<int,8>
    A *attr = new(&data)A;
    std::cout << typeid(attr).name() << std::endl;//class A * __ptr64
    A *attr2 = new A;
    std::cout << typeid(attr2).name() << std::endl;//class A * __ptr64
    return 0;
}

thread_local

c++11引入thread_local,用thread_local修饰的变量具有thread周期,每一个线程都拥有并只拥有一个该变量的独立实例,一般用于需要保证线程安全的函数中。

对于一个线程私有变量,一个线程拥有且只拥有一个该实例,类似于static。

基础数值类型

c++11新增了几种数据类型:long long、char16_t、char32_t等

char 类型是 C 和 C++ 中的原始字符类型。 char 类型可用于存储 ASCII 字符集或任何 ISO-8859 字符集中的字符,以及多字节字符的单个字节,例如 Shift-JIS 或 Unicode 字符集的 UTF-8 编码。 在 Microsoft 编译器中,char 是 8 位类型。 它是与 signed char 和 unsigned char 都不同的类型。 默认情况下,char 类型的变量将提升到 int,就像是从 signed char 类型一样,除非使用 /J 编译器选项。 在 /J 的情况下,它们被视为 unsigned char 类型并提升为 int (没有符号扩展)。

类型 unsigned char 通常用于表示 byte,它不是 C++ 中的内置类型。

wchar_t 类型是实现定义的宽字符类型。 在 Microsoft 编译器中,它表示一个 16 位宽字符,用于存储编码为 UTF-16LE 的 Unicode(Windows 操作系统上的本机字符类型)。 通用 C 运行时 (UCRT) 库函数的宽字符版本使用 wchar_t 及其指针和数组类型作为参数和返回值,本机 Windows API 的宽字符版本也是如此。

char8_t、char16_t 和 char32_t 类型分别表示 8 位、16 位和 32 位宽字符。 (char8_t 是 C++20 中的新增功能,需要 /std:c++20 或 /std:c++latest 编译器选项。)编码为 UTF-8 的 Unicode 可以存储在 char8_t 类型中。 char8_t 和 char 类型的字符串称为“窄”字符串,即使用于编码 Unicode 或多字节字符。 编码为 UTF-16 的 Unicode 可以存储在 char16_t 类型中,而编码为 UTF-32 的 Unicode 可以存储在 char32_t 类型中。 这些类型和 wchar_t 类型的字符串都称为“宽”字符串,但该术语通常特指 wchar_t 类型的字符串。

在 C++ 标准库中,basic_string 类型专用于窄字符串和宽字符串。 字符的类型为 char 时,使用 std::string;字符的类型为 char8_t 时,使用 std::u8string;字符的类型为 char16_t 时,使用 std::u16string;字符的类型为 char32_t 时,使用 std::u32string;而字符的类型为 wchar_t 时,使用 std::wstring。 其他表示文本的类型(包括 std::stringstream 和 std::cout)均可专用于窄字符串和宽字符串。

随机数功能

c++11关于随机数功能则较之前丰富了很多,典型的可以选择概率分布类型,先看如下代码:

#include <time.h>
#include <iostream>
#include <random>
using namespace std;
int main() {
    std::default_random_engine random(time(nullptr));
    std::uniform_int_distribution<int> int_dis(0, 100); // 整数均匀分布
    std::uniform_real_distribution<float> real_dis(0.0, 1.0); // 浮点数均匀分布
    for (int i = 0; i < 10; ++i) {
        cout << int_dis(random) << ' ';
    }
    cout << endl;
    for (int i = 0; i < 10; ++i) {
        cout << real_dis(random) << ' ';
    }
    cout << endl;
    return 0;
}

代码中举例的是整数均匀分布和浮点数均匀分布,c++11提供的概率分布类型还有好多,例如伯努利分布、正态分布等,具体可以见最后的参考资料。

目录
相关文章
|
2月前
|
Linux 编译器 测试技术
【C++】CentOS环境搭建-快速升级G++版本
通过上述任一方法,您都可以在CentOS环境中高效地升级G++至所需的最新版本,进而利用C++的新特性,提升开发效率和代码质量。
191 64
|
2月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
128 59
|
2月前
|
Linux 编译器 测试技术
【C++】CentOS环境搭建-快速升级G++版本
通过上述任一方法,您都可以在CentOS环境中高效地升级G++至所需的最新版本,进而利用C++的新特性,提升开发效率和代码质量。
237 63
|
27天前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
22 2
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
43 0
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(三)
【C++】面向对象编程的三大特性:深入解析继承机制
|
2月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(二)
【C++】面向对象编程的三大特性:深入解析继承机制