分类目录归档:C++语法

智能指针

std::unique_ptr

代码如下:

#include <iostream>
#include <memory>


int main() {
    {
        // 创建 unique_ptr

        auto myIntSmartPtr {std::make_unique<int>(10)};

        std::cout << *myIntSmartPtr << std::endl;
        *myIntSmartPtr = 33;
        std::cout << *myIntSmartPtr << std::endl;
    }

    class Simple {
    public:
        Simple() {std::cout << "Simple() called!" << std::endl;}
        ~Simple() {std::cout << "~Simple() called!" << std::endl;}
        void go() {std::cout << "go() called!" << std::endl;}
    };

    {
        // 创建 unique_ptr

        // 方法1
        auto mySimpleSmartPtr {std::make_unique<Simple>()};
        mySimpleSmartPtr->go();

        // 方法2
        std::unique_ptr<Simple> mySimpleSmartPtr2 {new Simple{}};
        mySimpleSmartPtr2->go();
    }

    {
        // 从 unique_ptr 获取原始指针
        // 使用成员函数 get() 访问底层指针

        auto processData{[](Simple* simple){
            simple->go();
        }};

        auto mySimpleSmartPtr {std::make_unique<Simple>()};
        processData(mySimpleSmartPtr.get());
    }

    {
        // 释放 unique_ptr 底层指针
        // 成员函数 reset(),两种用途:
        // 释放底层资源,将底层指针设置为nullptr
        // 释放底层资源,将底层指针指向新的对象实例
        auto mySimpleSmartPtr {std::make_unique<Simple>()};
        
        mySimpleSmartPtr.reset();
        if (mySimpleSmartPtr == nullptr) {std::cout << "mySimpleSmartPtr == nullptr" << std::endl;}

        mySimpleSmartPtr.reset(new Simple{});
        mySimpleSmartPtr->go();
    }

    {
        // 断开 unique_ptr 与 底层指针 的连接
        // 使用成员函数 release()
        // unique_ptr 失去了对资源的所有权,所以就不再管理资源,所以需要手动释放资源
        // unique_ptr 此时被设置为 nullptr
        auto mySimpleSmartPtr {std::make_unique<Simple>()};
        Simple* simple {mySimpleSmartPtr.release()};

        // 需要手动释放资源了
        delete simple;
        simple = nullptr;
    }

    {
        // 移动 unique_ptr 的所有权
        // 将 unique_ptr 智能指针作为类的数据成员

        class Foo {
        public:
            Foo(std::unique_ptr<int> data) : m_data {std::move(data)} {}
        private:
            std::unique_ptr<int> m_data;
        };

        auto myIntSmartPtr {std::make_unique<int>(42)};
        Foo f {std::move(myIntSmartPtr)};
    }

    {
        // 使用 unique_ptr 管理动态分配的 C 风格数组
        auto myVariableSizedArray {std::make_unique<int[]>(10)};  // 含有 10 个整型元素的C风格数组
        // myVariableSizedArray 的类型是 std::unique_ptr<int[]>

        myVariableSizedArray[1] = 123;
    }

    {
        // 自定义删除器
        // 默认 unique_ptr 使用标准 new 和 delete 运算符来分配和释放内存
        // 可以修改默认行为,使用自定义的分配和释放函数
        auto my_alloc {[](int value)->int* {
            std::cout << "my_alloc() called!" << std::endl;
            return new int {value};
        }};

        auto my_free {[](int* p){
            std::cout << "my_free() called!" << std::endl;
            delete p;
        }};

        std::unique_ptr<int, decltype(my_free)> myIntSmartPtr {my_alloc(10), my_free};
    
    }

    return 0;
}

std::shared_ptr

#include <iostream>
#include <memory>


int main() {
    class Simple {
    public:
        Simple() {std::cout << "Simple() called!" << std::endl;}
        ~Simple() {std::cout << "~Simple() called!" << std::endl;}
        void go() {std::cout << "go() called!" << std::endl;}
    };

    {
        // 创建 shared_ptr
        auto mySimpleSmartPtr {std::make_shared<Simple>()};
    }

    {
        // 自定义删除器
        // 和 unique_ptr 一样,默认 shared_ptr 使用标准 new 和 delete 运算符来分配和释放内存
        // 相比 unique_ptr 更方便一点,不需要将自定义删除器的类型指定为模板类型参数

        auto my_alloc {[](int value)->int* {
            std::cout << "my_alloc() called!" << std::endl;
            return new int {value};
        }};

        auto my_free {[](int* p){
            std::cout << "my_free() called!" << std::endl;
            delete p;
        }};

        std::shared_ptr<int> myIntSmartPtr {my_alloc(10), my_free};
    }

    {
        // 用 shared_ptr 储存旧式 C 风格文件指针(不需要再手动关闭文件)

        auto close {[](FILE* filePtr){
            if (filePtr == nullptr) {return;}
            fclose(filePtr);
            std::cout << "文件已关闭" << std::endl;
        }};

        FILE* f {fopen("data.txt", "w")};
        std::shared_ptr<FILE> filePtr {f, close};

        if (filePtr == nullptr) {std::cerr << "打开文件失败" << std::endl;}
        else {std::cout << "文件已打开" << std::endl;}
    }

    {
        // 别名(aliasing),
        // 让一个 shared_ptr 共享另一个 shared_ptr 底层的指针(记为 A)的所有权,
        // 但是这个 shared_ptr 却可以指向另一个对象,这个对象与 A 指向的对象不是同一个。

        class Foo {
        public:
            Foo(int value) : m_data {value} {}
            int m_data;
        };

        auto foo {std::make_shared<Foo>(42)};
        auto aliasing {std::shared_ptr<int> {foo, &foo->m_data}};
        // 智能指针 aliasing 与 智能指针 foo 共享对底层指针(记为A,官方名称 owned pointer)的所有权,
        // 但智能指针 aliasing 却指向另一个对象(官方名称 stored pointer),在这里是 A 指向的对象的数据成员 m_data。
        // 当两个 shared_ptr (foo 和 aliasing)都被销毁时,`Foo`对象才会被销毁。
        // "owned pointer"用于引用计数,"stored pointer"用于对智能指针解引用、对智能指针调用`get()`时返回

        std::cout << foo->m_data << std::endl;  // foo 指向一个 Foo 对象
        std::cout << *aliasing << std::endl;  // aliasing 指向 foo 指向的 Foo 对象的数据成员 m_data
    }

    return 0;
}

std::weak_ptr

是什么?为什么?怎么用?

#include <iostream>
#include <memory>


int main() {

    {
        // 演示 shared_ptr 无法正确处理的一种情况,引入 weak_ptr

        class Simple {
        public:
            Simple() {std::cout << "Simple() called!" << std::endl;}
            ~Simple() {std::cout << "~Simple() called!" << std::endl;}
            std::shared_ptr<Simple> m_ptr;
        };

        auto mySimpleSmartPtr1 {std::make_shared<Simple>()};
        auto mySimpleSmartPtr2 {std::make_shared<Simple>()};

        mySimpleSmartPtr1->m_ptr = mySimpleSmartPtr2;
        mySimpleSmartPtr2->m_ptr = mySimpleSmartPtr1;

        // 出现的情况是,两个对象能够正常构造,却没有正常析构。
        // mySimpleSmartPtr2 被 mySimpleSmartPtr1 使用,所以 mySimpleSmartPtr2 指向的对象的引用计数为 1
        // mySimpleSmartPtr1 被 mySimpleSmartPtr2 使用,所以 mySimpleSmartPtr1 指向的对象的引用计数为 1
        // 两者互相被对方使用,两者指向的对象的引用计数都不会降为0,对方不析构,自己就不能析构,
        // 所以两者指向的对象的析构函数都不会被调用
    }

    {
        // 解决上一个程序中 shared_ptr 无法正确处理的一种情况
        // 将类 Simple 的数据成员 m_ptr 的类型由 std::shared_ptr<Simple> 改为 std::weak_ptr<Simple> 可解决
        class Simple {
        public:
            Simple() {std::cout << "Simple() called!" << std::endl;}
            ~Simple() {std::cout << "~Simple() called!" << std::endl;}
            std::weak_ptr<Simple> m_ptr;  // !!! 这里变动,其他未变
        };

        auto mySimpleSmartPtr1 {std::make_shared<Simple>()};
        auto mySimpleSmartPtr2 {std::make_shared<Simple>()};

        mySimpleSmartPtr1->m_ptr = mySimpleSmartPtr2;
        mySimpleSmartPtr2->m_ptr = mySimpleSmartPtr1;

        // 为什么能解决?
        // weak_ptr 可以包含一个由 shared_ptr 管理的资源的引用,
        // weak_ptr 可以访问这个资源,但是不拥有这个资源的所有权,不会阻止 shared_ptr 释放该资源
    }

    class Simple {
    public:
        Simple() {std::cout << "Simple() called!" << std::endl;}
        ~Simple() {std::cout << "~Simple() called!" << std::endl;}
        void go() {std::cout << "go() called!" << std::endl;}
    };

    {
        // 创建 weak_ptr
        // weak_ptr 的构造函数需要一个 shared_ptr 或一个 weak_ptr 作为参数

        auto sharedSimple {std::make_shared<Simple>()};
        std::weak_ptr<Simple> weakSimple {sharedSimple};  // 这里从一个 shared_ptr 创建 weak_ptr
    }

    {
        // 使用 weak_ptr

        auto useResource{[](std::weak_ptr<Simple>& weakSimple){
            // 成员函数 lock() 返回 shared_ptr
            // 若 shared_ptr 管理的资源已被释放,则返回的 shared_ptr 底层为 nullptr
            auto resource {weakSimple.lock()};
            if (resource) {std::cout << "资源当前仍然存活。" << std::endl;}
            else {std::cout << "资源已经被释放" << std::endl;}
        }};

        auto sharedSimple {std::make_shared<Simple>()};
        std::weak_ptr<Simple> weakSimple {sharedSimple};  // 这里从一个 shared_ptr 创建 weak_ptr

        // 传入 weak_ptr,与该 weak_ptr 关联的 shared_ptr 管理的资源未释放
        useResource(weakSimple);

        // 传入 weak_ptr,与该 weak_ptr 关联的 shared_ptr 管理的资源已被释放
        sharedSimple.reset();
        useResource(weakSimple);

    }

    return 0;
}

日常使用

#include <iostream>
#include <memory>


int main() {

    {
        // 从函数返回智能指针
        // 直接按值返回即可,因为有 copy elision,可以理解为编译器有优化,很高效

        class Simple {
        public:
            Simple() { std::cout << "Simple() called!" << std::endl; }
            ~Simple() { std::cout << "~Simple() called!" << std::endl; }
            void go() { std::cout << "go() called!" << std::endl; }
        };

        auto create{ []() -> std::unique_ptr<Simple> {
            auto ptr {std::make_unique<Simple>()};
            // Do something with ptr...
            return ptr;
        } };

        // 方式1
        std::unique_ptr<Simple> mySmartPtr1{ create() };

        // 方式2,使用auto关键字
        auto mySmartPtr2{ create() };
    }

    {
        // 让一个对象可以返回一个指向自身的 shared_ptr 或 weak_ptr
        // 需要让类继承自 enable_shared_from_this<>

        class Foo : public std::enable_shared_from_this<Foo> {
        public:
            Foo() { std::cout << "Foo() called!" << std::endl; }
            ~Foo() { std::cout << "~Foo() called!" << std::endl; }
            std::shared_ptr<Foo> getPointer() {
                return shared_from_this();
            }
        };

        // 必须要先创建一个 shared_ptr,才能调用成员函数 shared_from_this()
        // (不一定通过这个 shared_ptr 来调用 shared_from_this(),但要有至少这么一个shared_ptr。)
        auto ptr1{ std::make_shared<Foo>() };
        auto ptr2{ ptr1->getPointer() };

        // 始终可以在一个对象上调用成员函数 weak_from_this()
        // 但如果在这个对象上还没有创建过一个 shared_ptr,返回的 weak_ptr 可能是空的

        // 函数:判断一个 weak_ptr 是不是空的
        auto isWeakPtrEmpty{ [](std::weak_ptr<Foo> const& weak_ptr) {
            if (weak_ptr.expired()) { std::cout << "weak_ptr 是空的" << std::endl; }
            else { std::cout << "weak_ptr 不是空的" << std::endl; }
        } };

        Foo* fooPtr{ new Foo{} };
        // 此时在 fooPtr 指向的对象上没有创建 shared_ptr,所以此时 weak_ptr 是空的
        auto ptrWeak{ fooPtr->weak_from_this() };
        isWeakPtrEmpty(ptrWeak);

        // 创建 shared_ptr
        auto ptrShared{ std::shared_ptr<Foo>{fooPtr} };
        // 此时 weak_ptr 不是空的
        ptrWeak = ptrShared->weak_from_this();
        isWeakPtrEmpty(ptrWeak);

    }

    {
        // 智能指针 与 C风格函数 一起使用
        // C 风格函数,喜欢通过返回值返回一个整数,以此判断函数是否正确运行;
        // 而真正要得到的数据,通过参数来返回(这样的参数叫做“输出参数”)

        using errorcode = int;
        auto my_alloc{ [](int value, int** data) -> errorcode {
            *data = new int {value};
            return 0;
        } };
        auto my_free{ [](int* data) {
            delete data;
            return 0;
        } };

        // 怎样用智能指针管理 通过参数返回的 动态内存?

        // C++23 前:
        std::unique_ptr<int, decltype(my_free)> myIntSmartPtr{ nullptr, my_free };
        int* data{ nullptr };
        my_alloc(42, &data);
        myIntSmartPtr.reset(data);

        std::cout << *myIntSmartPtr << std::endl;


        // 能否改成下面这样?:
        int* data1{ nullptr };
        my_alloc(42, &data1);
        std::unique_ptr<int, decltype(my_free)> myIntSmartPtr2{ data1, my_free };
        // 看下一个代码片段(下一个大花括号),认为两种声明 unique_ptr 的方式是等价的,都有效。
        // 我想,作者这么写,可能是为了和下面他要介绍的C++23的书写方式的语句顺序保持一致。

        std::cout << *myIntSmartPtr2 << std::endl;

        // C++23:
        std::unique_ptr<int, decltype(my_free)> myIntSmartPtr3{ nullptr, my_free };
        my_alloc(42, std::inout_ptr(myIntSmartPtr3));

        std::cout << *myIntSmartPtr2 << std::endl;
    }

    {
        // 实验 上一个代码片段,两种声明 unique_ptr 的方式是否等价
        class Simple {
        public:
            Simple() { std::cout << "Simple() called!" << std::endl; }
            ~Simple() { std::cout << "~Simple() called!" << std::endl; }
            void go() { std::cout << "go() called!" << std::endl; }
        };

        using errorcode = int;
        auto my_alloc{ [](Simple** data) -> errorcode {
            *data = new Simple {};
            return 0;
        } };
        auto my_free{ [](Simple* data) {
            delete data;
            return 0;
        } };

        // C++23 前:
        std::unique_ptr<Simple, decltype(my_free)> mySimpleSmartPtr{ nullptr, my_free };
        Simple* data{ nullptr };
        my_alloc(&data);
        mySimpleSmartPtr.reset(data);

        // 能否改成这样:
        Simple* data1{ nullptr };
        my_alloc(&data1);
        std::unique_ptr<Simple, decltype(my_free)> myIntSmartPtr2{ data1, my_free };

        // 经测试,用这两种方式创建的 unique_ptr 都能正常释放内存
        // 应该可以说明,二者是等价的
    }

    return 0;
}

内存陷阱

这里总结的常见内存陷阱有:

  • 数据缓冲区分配不足,导致缓冲区溢出
  • 越界内存访问
  • 内存泄漏
  • 重复 delete 指针

代码如下:

#include <iostream>
#include <cstring>

int main() {
    // 内存陷阱 memory pitfalls
    {
        // 数据缓冲区分配不足,导致缓冲区溢出
        char buffer[1024] {0};
        char* nextChunk {new char[1025]{0}};
        strcat(buffer, nextChunk);  // bug!
    }

    {
        // 越界内存访问
        // 对于函数 fillWithM(),如果参数 text 指向的字符串末尾的'\0'丢失了
        // 则字符串边界之外的内存也会被修改
        auto fillWithM {[](char* text){
            for (int i{0}; text[i] != '\0'; ++i) {
                text[i] = 'm';
            }
        }};

    }

    {
        // 内存泄漏
        // 在函数 doSomething() 中将指针指向了新的地址,而原地址的内存未释放
        // 所以原来的 Simple 对象以及它的成员指针 m_intPtr 指向的 int 值所占的内存就发生了泄漏
        
        class Simple {
        public:
            Simple() {m_intPtr = new int{};}  // !!!
            ~Simple() {delete m_intPtr;}
            void setValue(int value) {*m_intPtr = value;}
        private:
            int* m_intPtr;
        };

        auto doSomething{[](Simple*& outSimplePtr){
            outSimplePtr = new Simple{};  // bug! 原来的对象未 delete
        }};

        Simple* simplePtr {new Simple{}};
        doSomething(simplePtr);  // !!!
        delete simplePtr;  // 只清理了第二个对象(在函数 doSomething() 中 new 的对象)
    }

    {
        // 重复删除 Double-Deletion,悬空指针 Dangling pointer
        int* myInt {new int{5}};
        delete myInt;  // delete,此时 myInt 是悬空指针,即它指向的地址已经不归当前程序管辖了,对当前程序而言这个地址是一个无效的地址
        delete myInt;  // 又一次delete
        // 如果 myInt 指向的内存在第一次 delete 之后被其他程序使用了, 
        // 第二次 delete 就是问题了,程序可能会释放已分配给其他程序的内存。
        // 正确的做法是在 delete 后立即将指针赋值为 nullptr
    }

    return 0;
}

数组与指针的关系

数组名可以看作是指向数组中第一个元素的指针。

在向函数传递数组时,可以通过指针传递,也可以通过引用传递。

代码如下:

#include <iostream>

int main() {
    {
        // 数组退化(Decay)为指针

        int myIntArray[10] {};
        int* myIntPtr {myIntArray};

        // 通过指针访问数组元素
        myIntPtr[4] = 5;
    }

    {
        // 使用指针将数组传递给函数

        /*
        如下三种参数的声明等价:
        int* theArray
        int theArray[]
        int theArray[2]  // 方括号内的数字会被忽略
        */
        auto doubleInts {[](int* theArray, std::size_t size){
            for (std::size_t i{}; i < size; ++i) {
                theArray[i] *= 2;
            }
        }};

        // 传递自由存储区中的数组
        std::size_t arrSize{4};
        int* freeStoreArray {new int[arrSize]{1,5,3,4}};
        doubleInts(freeStoreArray, arrSize);
        delete [] freeStoreArray;
        freeStoreArray = nullptr;

        // 传递栈上的数组
        int stackArray[] {1,5,3,4};
        std::size_t arrSize2 {std::size(stackArray)};  // Since C++17
        // std::size_t arrSize2 {sizeof(stackArray) / sizeof(stackArray[0])};  // Pre-C++17
        doubleInts(stackArray, arrSize2);
        // 等价于:
        doubleInts(&stackArray[0], arrSize2);
    }

    {
        // 使用引用将数组传递给函数

        // 该函数只接收大小为 4 的基于栈的数组
        auto doubleInts {[](int (&theArray)[4]){
            for (std::size_t i{}; i < std::size(theArray); ++i) {
                theArray[i] *= 2;
            }
        }};

        int stackArray[] {1,5,3,4};
        doubleInts(stackArray);

        // int stackArray2[] {1,5,3};
        // doubleInts(stackArray2);  // 将报错,因为数组不符合要求
    }

    {
        // 使用引用将数组传递给函数

        // 函数模板,让编译器自动推断栈上数组的大小
        auto doubleInts {[]<std::size_t N>(int (&theArray)[N]){
            for (std::size_t i{}; i < N; ++i) {
                theArray[i] *= 2;
            }
        }};
        // 等价于常规写法:
        //template<std::size_t N>
        //void doubleInts(int (&theArray)[N]) {
        //    for (std::size_t i{}; i < N; ++i) { theArray[i] *= 2; }
        //}

        int stackArray[] {1,5,3,4};
        doubleInts(stackArray);

        int stackArray2[] {1,5,3};
        doubleInts(stackArray2);

        for (auto item : stackArray) {std::cout << item << " ";}
        std::cout << std::endl;

        for (auto item : stackArray2) {std::cout << item << " ";}
        std::cout << std::endl;
    }

    return 0;
}

动态内存

代码包括:

  • 在自由存储区上分配内存
  • 指针只能位于栈上吗?否
  • 当内存分配失败,引发异常
  • 在栈上声明一个数组
  • 在自由存储区上声明一个数组
  • 在自由存储区上声明一个二维数组

代码如下:

#include <iostream>

int main() {
    {
        // 在 堆 上分配内存
        
        //int* ptr {nullptr};
        //ptr = new int;
        int* ptr {new int};

        delete ptr;
        ptr = nullptr;
    }

    {
        // 指针只能位于栈上吗?否

        int** handle {nullptr};
        handle = new int*;
        *handle = new int;

        // handle 是一个位于栈上的指针
        // *handle 是一个位于堆上的指针

        delete *handle;
        delete handle;
        handle = nullptr;
    }

    {
        // 当内存分配失败,引发异常
        // 使用 new(nothrow) 分配内存,不会引发异常,而是返回 nullptr

        int* ptr {new(std::nothrow) int};

        delete ptr;
        ptr = nullptr;
    }

    {
        // 在栈上声明一个数组
        // 数组中各个元素未初始化
        int myArray[5];
    }

    {
        // 在栈上声明一个数组,使用初始化列表为元素提供初始值
        //int myArray[5] {1,2,3,4,5};
        int myArray[] {1,2,3,4,5};
    }

    {
        // 在栈上声明一个数组,使用初始化列表为元素提供初始值
        // 若初始化列表包含的元素少于数组的大小,后面的元素将进行零初始化
        int myArray[5] {1,2};  // 1,2,0,0,0
    }

    {
        // 在栈上声明一个数组,将所有元素都初始化为零
        int myArray[5] {};  // 0,0,0,0,0
    }

    {
        // 在自由存储区上声明一个数组
        // 数组中各个元素未初始化
        int* myArrayPtr {new int[5]};

        delete[] myArrayPtr;
        myArrayPtr = nullptr;
    }

    {
        // 在自由存储区上声明一个数组
        // 使用 new(nothrow), 分配内存失败不会引发异常,而是返回 nullptr

        int* myArrayPtr {new(std::nothrow) int[5]};

        delete [] myArrayPtr;
        myArrayPtr = nullptr;
    }

    {
        // 在自由存储区上声明一个数组
        // 使用初始化列表为元素提供初始值
        int* myArrayPtr {new int[] {1,2,3,4,5}};

        delete [] myArrayPtr;
        myArrayPtr = nullptr;
    }

    {
        // 在自由存储区上声明一个数组
        // 数组的大小是动态的,在运行时确定
        class Document {};
        auto askUserForNumberOfDocuments {[](){return 5;}};
        std::size_t numberOfDocuments {askUserForNumberOfDocuments()};

        Document* documents {new Document[numberOfDocuments]{}};

        delete [] documents;
        documents = nullptr;
    }

    class Simple {
    public:
    Simple() {std::cout << "Simple constructor called!" << std::endl;}
    ~Simple() {std::cout << "Simple destructor called!" << std::endl;}
    };

    {
        // 在自由存储区上声明一个数组,数组由对象组成
        // new[] 会自动为每个对象调用零参(默认)构造函数
        // 对于基本类型的元素则不会,基本类型数组默认具有未初始化的元素

        Simple* mySimpleArray {new Simple[5]};
        // 等价于:
        //Simple* mySimpleArray {new Simple[5]{}};

        delete [] mySimpleArray;
        mySimpleArray = nullptr;
    }

    {
        // 在自由存储区上声明一个指针数组,数组元素指向各个对象

        // new
        const std::size_t arraySize{4};
        Simple** mySimplePtrArray {new Simple*[arraySize]};

        for (std::size_t i{0}; i < arraySize; ++i) {
            mySimplePtrArray[i] = new Simple;  // 等价于 new Simple{};
        }

        // delete
        for (std::size_t i{0}; i < arraySize; ++i) {
            delete mySimplePtrArray[i];
            mySimplePtrArray[i] = nullptr;
        }

        delete [] mySimplePtrArray;
        mySimplePtrArray = nullptr;
    }

    {
        // 在自由存储区上声明一个二维数组
        // 先为第一维度分配内存,然后为各个子数组分配内存
        const std::size_t xDimension {5};
        const std::size_t yDimension {4};

        // new
        char** myArray{new char*[xDimension]};
        for (std::size_t i{0}; i < xDimension; ++i) {
            myArray[i] = new char[yDimension];
        }

        // delete
        for (std::size_t i{0}; i < xDimension; ++i) {
            delete [] myArray[i];
            myArray[i] = nullptr;
        }
        delete [] myArray;
        myArray = nullptr;
    }

    {
        // 在自由存储区上声明一个二维数组,
        // 使用连续的内存块,大小为 xDimension * yDimension
        // 使用公式 x * xDimension + y 访问位置 (x, y) 的元素
        const std::size_t xDimension {5};
        const std::size_t yDimension {4};

        // new
        char* myArray {new char[xDimension * yDimension]};
        
        // 访问位置 (4, 3) 的元素(数组最后一个元素)
        const std::size_t pos {4 * xDimension + 3};
        myArray[pos] = 'A';

        // delete
        delete [] myArray;
        myArray = nullptr;
    }

    return 0;
}

Chapter 2 Exercises – Professional C++

练习 2-1:编写一个程序,要求用户输入两个字符串,然后使用三向比较运算符按字母顺序打印它们。要要求用户输入一个字符串,可以使用在第 1 章中简要介绍的 std::cin 流。第 13 章“揭秘 C++ I/O”详细解释了输入和输出,但现在,以下是有关如何从控制台中读取字符串的信息。要终止该行,只需按 Enter 键。

std::string s;
getline(cin, s1);

我的解答:

#include <iostream>
#include <string>

int main() {
	std::string s1;
	std::getline(std::cin, s1);
	
	std::string s2;
	std::getline(std::cin, s2);

	auto result{ s1 <=> s2 };

	if (std::is_lt(result)) {
		std::cout << s1 << std::endl;
		std::cout << s2 << std::endl;
	}
	else {
		std::cout << s2 << std::endl;
		std::cout << s1 << std::endl;
	}

	return 0;
}

练习 2-2:编写一个程序,要求用户输入一个源字符串(= 干草堆)、一个要在源字符串中查找的字符串(= 针)和一个替换字符串。编写一个具有三个参数(干草堆、针和替换字符串)的函数,该函数返回一个干草堆的副本,其中所有针都用替换字符串替换。仅使用 std::string ,不用 string_view 。你会使用什么类型的参数,以及为什么选择这种类型?从 main() 调用此函数,并打印出所有字符串以进行验证。

我的解答:

#include <iostream>
#include <string>

std::string process(std::string haystack, std::string const& needle, std::string const& replacement) {
	if (needle == replacement) {
		return haystack;
	}
	
	std::size_t posNeedle{ 0 };
	while ((posNeedle = haystack.find(needle, posNeedle)) != haystack.npos) {
		haystack.replace(posNeedle, needle.size(), replacement);
		posNeedle += replacement.size();
	}
	
	return haystack;
}

int main() {
	std::string strSrc;
	std::string strFind;
	std::string strReplace;

	std::cout << "输入源字符串: ";
	std::getline(std::cin, strSrc);

	std::cout << "输入要在源字符串中查找的字符串: ";
	std::getline(std::cin, strFind);

	std::cout << "输入替换字符串: ";
	std::getline(std::cin, strReplace);


	std::string result{ process(strSrc, strFind, strReplace) };

	std::cout << "Haystack: " << strSrc << std::endl;
	std::cout << "Needle: " << strFind << std::endl;
	std::cout << "Replacement: " << strReplace << std::endl;
	std::cout << "Result: " << result << std::endl;

	return 0;
}

对于函数的三个参数,我使用的类型分别是std::stringstd::string const&std::string const&。对于后两个参数,由于不会在函数中对它们进行修改,所以使用了 const 引用。对于第一个参数,由于题目中要求返回该参数的副本,因此我直接使用了 std::string,在调用函数时,该参数会进行一次拷贝,所以在函数内部对该变量的修改不会影响原始变量,同时也省去了在函数内部再次显式拷贝字符串的代码。在函数返回时,我也是直接返回的 std::string,而不是 std::move(std::string),原因是编译器对此有优化,如果写成后者反倒是画蛇添足。

练习 2-3:修改练习 2-2 中的程序,并在尽可能多的位置使用 std::string_view

我的解答:

#include <iostream>
#include <string>

std::string process(std::string haystack, std::string_view needle, std::string_view replacement) {
	if (needle == replacement) {
		return haystack;
	}
	
	std::size_t posNeedle{ 0 };
	while ((posNeedle = haystack.find(needle, posNeedle)) != haystack.npos) {
		haystack.replace(posNeedle, needle.size(), replacement);
		posNeedle += replacement.size();
	}
	
	return haystack;
}

int main() {
	std::string strSrc;
	std::string strFind;
	std::string strReplace;

	std::cout << "输入源字符串: ";
	std::getline(std::cin, strSrc);

	std::cout << "输入要在源字符串中查找的字符串: ";
	std::getline(std::cin, strFind);

	std::cout << "输入替换字符串: ";
	std::getline(std::cin, strReplace);


	std::string result{ process(strSrc, strFind, strReplace) };

	std::cout << "Haystack: " << strSrc << std::endl;
	std::cout << "Needle: " << strFind << std::endl;
	std::cout << "Replacement: " << strReplace << std::endl;
	std::cout << "Result: " << result << std::endl;

	return 0;
}

可以改成 std::string_view 的地方不多,我就只把函数的后两个参数的类型改了。第一个参数,我想了想,感觉没必要,感觉现在这样就很优雅了。我特别想用 std::string_view 的成员函数 remove_prefix(),可以用来跳过字符串的前面已查找过的部分,但是由于 std::string_view 只是一个视图,不能修改字符串,所以用在这里不适合。

这个成员函数remove_prefix()我在一个类似的程序中用过,那个程序是查找一个字符串在另一个字符串中出现的次数,见:std::string_view 类

练习 2-4:编写一个程序,要求用户输入未知数量的浮点数,并将所有数字存储在 vector 中。每个数字输入后都应换行。当用户输入数字 0 时,停止要求输入更多数字。要从控制台读取浮点数,请以与在第 1 章中输入整数值相同的方式使用 cin 。在具有几列的表格中设置所有数字的格式,其中每列以不同的格式输出数字。表格中的每一行对应一个输入的数字。

我的解答:

import std;

int main() {
	double value;
	std::vector<double> vec;
	
	std::cin >> value;
	while (value != 0.0) {
		vec.push_back(value);
		std::cin >> value;
	}

	for (double value : vec) {
		// 默认;科学计数法;定点计数法;通用计数法;十六进制计数法
		//std::println("{0}\t{0:e}\t{0:f}\t{0:g}\t{0:a}", value);

		std::println("{0:16e} | {0:12f} | {0:<12g} | {0:>+#12g}", value);
		// +,表示对负数和正数显示符号。
		// #,对于浮点数类型,将始终输出一个十进制分隔符(decimal separator),
		// 如`3`,将显示`3.000000`(0的个数依赖其他设置),3 后面的`.`将始终显示
	}

	return 0;
}

练习 2-5:编写一个程序,要求用户输入未知数量的单词。当用户输入 `*` 时,停止输入。将所有单个单词存储在 `vector` 中。您可以使用以下内容输入单个单词:

std::string word;
cin>> word;

我的解答:

import std;

int main() {
	std::string word;
	std::vector<std::string> vec;

	while (true) {
		std::cin >> word;
		if (word == "*") {
			break;
		}

		vec.push_back(std::move(word));
	}

	// 使用算法 std::max_element() 查找最长字符串的迭代器
	auto longest_str{ std::max_element(vec.begin(), vec.end(), [](std::string_view a, std::string_view b) {return a.size() < b.size(); }) };
	// 最长字符串的长度
	std::size_t longest_length{ longest_str != vec.end() ? longest_str->size() : 0 };
	
	for (int i{}; i < vec.size(); ++i) {
		if ((i + 1) % 5 == 0) {
			std::println("|{:^{}}|", vec[i], longest_length);
		}
		else {
			std::print("|{:^{}}", vec[i], longest_length);
		}
	}

	return 0;
}

我的答案有点“杀鸡用牛刀”。

std::string_view 类

是什么?为什么?怎么用?

将 std::string_view 作为函数的参数的类型,在向函数传参时,既可以传 std::string,还可以传 C 风格字符串,就不用再因为字符串的类型有多种,而不得不写多个函数重载。

当函数需要一个只读字符串作为其参数之一时,推荐使用 std::string_view 代替 const string&const char*

返回字符串的函数应返回 const string& 或 string ,但不能返回 string_view 。返回 string_view 会带来使返回的 string_view 无效的风险。

代码如下:

#include <iostream>
#include <string>
#include <string_view>

// 提取文件的扩展名
std::string extractExtension(std::string_view filename) {
    // 为了防止意外复制 `string_view` 中的字符串,不能隐式地从 `string_view` 构造 `string`,
    // 因此这里使用显式的 `string` 构造函数
    return std::string {filename.substr(filename.rfind('.'))};
}

int main() {
    {
        // 可以向接收 std::string_view 类型参数的函数传多种类型的字符串
        
        // 传 std::string
        std::string filename{R"(c:\temp\my file.txt)"};
        std::cout << "C++ string: " << extractExtension(filename) << std::endl;

        // 传 const char*
        const char* filename2{R"(c:\temp\my file.txt)"};
        std::cout << "C string: " << extractExtension(filename2) << std::endl;

        // 传字符串字面量
        std::cout << "Literal: " << extractExtension(R"(c:\temp\my file.txt)") << std::endl;
    }

    {
        // 拼接 string 和 string_view
        std::string str1 {"hello"};
        std::string_view sv1 {" world"};

        // 方法1,不能直接用 + 运算符拼接 string 和 string_view
        // auto result {str1 + sv1};  // 编译出错
        auto result {str1 + std::string{sv1}};
        std::cout << result << std::endl;

        // 方法2,使用 string 的 append() 成员函数
        std::string result2 {str1};
        result2.append(sv1.data(), sv1.size());
        std::cout << result2 << std::endl;
    }

    return 0;
}

成员函数 remove_prefix()

代码参考自:std::basic_string_view<CharT,Traits>::remove_prefix – cppreference.com

#include <iostream>
#include <string>
#include <string_view>

// 计算一个字符串内,某子字符串出现的次数
std::size_t count_substrings(std::string_view hive, std::string_view const bee) {
    if (hive.empty() || bee.empty()) {
        return 0;
    }

    std::size_t buzz{};
    while (bee.size() <= hive.size()) {
        const auto pos {hive.find(bee)};
        if (pos == hive.npos) {
            break;
        }

        ++buzz;
        hive.remove_prefix(pos + bee.size());
    }

    return buzz;
}

int main() {
    const char* hive {"bee buzz bee buzz bee"};
    std::cout << count_substrings(hive, "bee") << " 个 bee 在 hive 中" << std::endl;

    return 0;
}

字符串数值转换

字符串 和 数值 之间的相互转换,有两种方式。一种是使用高级函数(在<string>中定义),一种是使用低级函数(在<charconv>中定义)。

高级数值转换(High-Level Numeric Conversions)

代码如下:

#include <iostream>
#include <string>

int main() {
    {
        // 数值 转换为 字符串
        // std::to_string()

        int i {345};
        std::string s1{std::to_string(i)};

        double d {3.14};
        std::string s2{std::to_string(d)};

        long double ld {3.14L};
        std::string s3{std::to_string(ld)};

        std::cout << s1 << " " << s2 << " " << s3 << std::endl;
    }

    {
        // 字符串 转换为 数值
        // 函数命名规则:s(字符串) to 数值类型简写
        /*
         * i, int, stoi()
         * l, long, stol()
         * ul, unsigned long, stoul()
         * ll, long long, stoll()
         * ull, unsigned long long, stoull()
         * f, float, stof()
         * d, double, stod()
         * ld, long double, stold()
         */
        const std::string toParse {"    123USD"};

        size_t index{0};  // 用于存放未能转换的字符中第一个字符的索引
        int value {std::stoi(toParse, &index)};

        std::cout << "转换结果:" << value << std::endl;
        std::cout << "第一个未能转换的字符:" << toParse[index] << std::endl;
        std::cout << "未能转换的字符:" << toParse.substr(index) << std::endl;
    }

    return 0;
}

低级数值转换(Low-Level Numeric Conversions)

什么是“完美往返”(perfect round-tripping)?将数值转换为字符串,然后将结果字符串再次转换为数值,所得到的结果与原始数值相同。

代码如下:

#include <iostream>
#include <string>
#include <charconv>

int main() {
    {
        // 整数数值 转换为 字符串
        // std::to_chars()
        /*
        返回类型为:
        struct to_chars_result {
            char* ptr;
            errc ec;
        };
        */

        int i {12345};

        // 缓冲区,大小为50,用空格初始化
        const size_t BufferSize {50};
        std::string out(BufferSize, ' ');  // 一个由 BufferSize 个空格组成的字符串

        // 普通写法
        auto result {std::to_chars(out.data(), out.data() + out.size(), i)};
        if (result.ec == std::errc{}) {  // 转换成功
            std::cout << out << std::endl;
        }

        // 使用 结构化绑定(structured bindings) 的写法
        auto [ptr, error] {std::to_chars(out.data(), out.data() + out.size(), i)};
        if (error == std::errc{}) {  // 转换成功
            std::cout << out << std::endl;
        }
    }

    {
        // 浮点数数值 转换为 字符串
        // std::to_chars()

        double value {0.314};

        // 缓冲区,大小为50,用空格初始化
        const size_t BufferSize {50};
        std::string out(BufferSize, ' ');  // 一个由 BufferSize 个空格组成的字符串

        auto [ptr, error] {std::to_chars(out.data(), out.data() + out.size(), value)};
        if (error == std::errc{}) {  // 转换成功
            std::cout << out << std::endl;
        }
    }

    {
        // 字符串 转换为 数值
        // std::from_chars()
        /*
        返回类型为:
        struct from_chars_result {
            const char* ptr;
            errc ec;
        };
        */

        const std::string toParse {"    123USD"};
        int result;

        // 由于 std::from_chars() 不会跳过左侧空格,所以在转换前应当先去除空格
        const std::string midValue {toParse.substr(4)};  // 只是演示,所以就只是手动去除了字符串左侧空格
        auto [ptr, error] {std::from_chars(midValue.data(), midValue.data() + midValue.size(), result)};
        if (error == std::errc{}) {  // 转换成功
            std::cout << result << std::endl;
        }
    }

    {
        // 完美往返
        // double 数值 转换为 字符串,然后从结果字符串转换为 double 数值,所得结果和原始数值相同。
        double value {0.314};

        // 缓冲区,大小为50,用空格初始化
        const size_t BufferSize {50};
        std::string out(BufferSize, ' ');  // 一个由 BufferSize 个空格组成的字符串

        auto [ptr1, error1] {std::to_chars(out.data(), out.data() + out.size(), value)};
        if (error1 == std::errc{}) {  // 转换成功
            std::cout << "double -> 字符串 成功:" << out << std::endl;
        }

        double resultValue;
        auto [ptr2, error2] {std::from_chars(out.data(), out.data() + out.size(), resultValue)};
        if (error2 == std::errc{}) {  // 转换成功
            std::cout << "字符串 -> double 成功:" << resultValue << std::endl;
        }

        if (resultValue == value) {
            std::cout << "完美往返" << std::endl;
        }
    }

    return 0;
}

命名空间

命名空间解决了不同代码片段之间的命名冲突问题。

#include <iostream>
#include <string>
#include <vector>

namespace mycode {
    void foo() {std::cout << "调用命名空间 mycode 内的 foo() 函数" << std::endl;}
    void bar() {std::cout << "调用命名空间 mycode 内的 bar() 函数" << std::endl;}

}

int main() {
    {
        // 调用命名空间内的函数
        mycode::foo();
        mycode::bar();
    }

    {
        // 使用 using 指令(directive)
        // 凡是命名空间 mycode 内的函数都可以只写函数名,不用写命名空间前缀
        using namespace mycode;
        foo();
        bar();
    }

    {
        // 使用 using 声明(declaration)
        // 只有命名空间 mycode 内的 foo() 函数可以只写函数名,不用写命名空间前缀
        // 命名空间 mycode 内的其他函数还是要写完整的命名空间前缀
        using mycode::foo;
        foo();
        mycode::bar();
    }

    return 0;
}

嵌套命名空间和命名空间别名

#include <iostream>
#include <string>
#include <vector>

// 嵌套命名空间,写法1,C++17之前
namespace mycode {
    namespace Networking {
        namespace FTP {
            void foo() {std::cout << "调用 foo() 函数" << std::endl;}

        }
    }
}

// 嵌套命名空间,写法2,语法更紧凑
namespace mycode::Networking::FTP {
    void bar() {std::cout << "调用 bar() 函数" << std::endl;}
}


int main() {
    {
        // 调用嵌套命名空间中的函数
        mycode::Networking::FTP::foo();
        mycode::Networking::FTP::bar();
    }

    {
        // 命名空间别名
        namespace MyFTP = mycode::Networking::FTP;
        
        // 二者效果相同,都是调用 foo() 函数:
        mycode::Networking::FTP::foo();  // 调用嵌套命名空间中的 foo() 函数
        MyFTP::foo();  // 用别名调用 foo() 函数
    }

    return 0;
}

内联命名空间

在内联命名空间(inline namespaces)中声明的东西,都会自动地在父级命名空间中可用。

如有命名空间mycode,其有子级内联命名空间Networking,我在Networking中声明的函数foo(),既可以这样调用:mycode::Networking::foo();也可以这样调用:mycode::foo(),就像直接把函数 foo() 声明在了命名空间mycode中一样。

如 std::string 字符串字面量。标准字面量 s 在命名空间 std::literals::string_literals 中定义。命名空间 string_literals 和 literals 是内联命名空间。所以,使用下面的 using 指令中的任意一条后,都可以在代码中使用标准字面量 s

using namespace std;
using namespace std::literals;
using namespace std::string_literals;
using namespace std::literals::string_literals;

std::string 字符串字面量的示例代码如下。来自 C++风格字符串,std:string 类

#include <iostream>
#include <string>
 
int main() {
    {
        // std::string 字符串字面量
        // 标准字面量

        using namespace std::literals::string_literals;  // 内联命名空间
        auto str {"hello world"s};  // str 类型是 std::string
    }
 
    return 0;
}

怎样定义自己的内联命名空间?使用 inline 关键字,代码如下:

#include <iostream>

namespace mycode {
    inline namespace Networking {
        inline namespace FTP {
            void foo() {std::cout << "调用 foo() 函数" << std::endl;}
        }
    }
}

int main() {
    // 由于 Networking 和 FTP 是内联命名空间,
    // 如下 3 种方式都可以调用 foo() 函数
    {
        using namespace mycode::Networking::FTP;
        foo();
    }

    {
        using namespace mycode::Networking;
        foo();
    }

    {
        using namespace mycode;
        foo();
    }

    return 0;
}

C++风格字符串,std:string 类

代码包含内容有:

  • 字符串拼接
  • 字符串比较
  • compare() 成员函数
  • 三向比较运算符 <=>
  • 访问字符串中的字符
  • 转换为C风格字符串
  • 字符串操作,包括:
  • substr(pos, len)
  • find(str)
  • replace(pos, len, str)
  • starts_with(str), ends_with(str)
  • contains(str), containns(ch)

代码如下:

#include <iostream>
#include <string>
#include <cstring>
#include <compare>

int main() {
    {
        // 拼接字符串
        std::string a{"12"};
        std::string b{"34"};

        // 重载的 + 运算符
        std::string c {a+b};  // 1234
        std::cout << c << std::endl;

        // 重载的 += 运算符
        a += b;  // 1234
        std::cout << a << std::endl;
    }

    {
        // 比较两个字符串
        std::string a {"Hello"};
        std::string b {"hello"};

        // 用重载的 ==, !=, >, <, >=, <= 比较
        const char* sign {a == b ? "==" : a > b ? ">" : "<"};
        std::cout << a << sign << b << std::endl;

        // compare() 成员函数
        // 行为类似于 strcmp(),返回值为 小于0的值; 0; 大于0的值
        int result {a.compare(b)};
        const char* sign2 {result == 0 ? "==" : result > 0 ? ">" : "<"};
        std::cout << a << sign << b << std::endl;

        // 三向比较运算符 <=>
        auto result2 {a<=>b};
        if (std::is_gt(result2)) {
            const char* sign3 {">"};
        }
        else if (std::is_lt(result2)) {
            const char* sign3 {"<"};

        }
        else if (std::is_eq(result2)) {
            const char* sign3 {"=="};
        }
        std::cout << a << sign << b << std::endl;
    }

    {
        // 访问字符串中各个字符,
        // 使用 方括号运算符 []
        // 像C风格字符串那样
        std::string a {"hello, world!"};
        std::cout << a << std::endl;
        a[0] = 'H';
        a[7] = 'W';
        std::cout << a << std::endl;
    }

    {
        // 转换为C风格字符串
        // 成员函数 c_str()
        // 成员函数 data()

        std::string a {"hello, world!"};
        const char* str1 {a.c_str()};
        char* str2 {a.data()};  // 在 C++14 及之前,data()返回 const char*
    }

    {
        // 字符串操作
        std::string a {"hello, world!"};

        // substr(pos, len)
        // 返回从给定位置开始且具有给定长度的子字符串
        std::string str1 {a.substr(0, 5)};
        std::cout << str1 << std::endl;

        // find(str)
        // 返回找到给定子字符串的位置,未找到则返回`string::npos`
        std::size_t pos1 {a.find("world")};
        if (pos1 != std::string::npos) {
            std::cout << "找到啦:" << pos1 << std::endl;
        }

        // replace(pos, len, str)
        // 用另一个字符串替换字符串的一部分(由位置和长度指定)
        std::string b {"hello, world!"};
        b.replace(7, 5, "Aoyu");
        std::cout << b << std::endl;

        // starts_with(str)
        // ends_with(str)
        // 如果字符串以给定的子字符串开头/结尾,则返回`true`
        if (a.starts_with("hello")) {
            std::cout << a << " 以 hello 开头" << std::endl;
        }
        if (a.ends_with("!")) {
            std::cout << a << " 以 ! 结尾" << std::endl;
        }

        // contains(str), containns(ch)
        // 如果`string`包含另一个`string`或字符,则返回`true`
        // C++23
        if (a.contains("wor")) {
            std::cout << a << " 包含 wor" << std::endl;
        }
        if (a.contains('h')) {
            std::cout << a << " 包含 h" << std::endl;
        }
    }

    return 0;
}

std::string 字符串字面量

按照C++标准规定,字符串字面量的类型为“n个const char的数组”。因此,字符串字面量通常被解释为const char*const char[]。可以使用标准字面量s将字符串字面量解释为 std::string

#include <iostream>
#include <string>

int main() {
    {
        // std::string 字符串字面量
        // 标准字面量

        auto str1 {"hello world"};  // str1 类型是 const char*
        auto& str2 {"hello world"};  // str2 类型是 const char[12]

        using namespace std::literals::string_literals;  // 内联命名空间
        auto str3 {"hello world"s};  // str3 类型是 std::string
    }

    return 0;
}

字符串向量的 CTAD

CTAD,类模板参数推断,用于自动推断模板类型参数,在使用类模板时不需要再手动在尖括号内指定类型参数。创建字符串向量时,需要注意,如代码:

#include <iostream>
#include <string>
#include <vector>

int main() {
    {
        // 字符串向量的 CTAD
        
        std::vector names1 {"XiaoMing", "XiaoHong", "XiaoLiang"};
        // 推断出的 names1 的类型为 std::vector<const char*>

        // 使用 std::string 字符串字面量
        using namespace std::literals;
        std::vector names2 {"XiaoMing"s, "XiaoHong"s, "XiaoLiang"s};
        // 推断出的 names2 的类型为 std::vector<std::string>
    }

    return 0;
}

字符串字面量

C++标准规定,字符串字面量的类型为“n个const char的数组”。

修改字符串字面量会怎么样?

答案:未定义行为。

#include <iostream>

int main() {
    char* str{"hello world!"};  // 将字符串字面量赋值给 char* 类型变量
    str[0] = 'g';  // 未定义行为

    return 0;
}

gcc13.2,上述程序在编译过程中给出警告,运行程序出错。

编译过程中的警告信息:

<source>: In function 'int main()':
<source>:4:15: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
    4 |     char* str{"hello world!"};  // 将字符串字面量赋值给 char* 类型变量
      |               ^~~~~~~~~~~~~~
ASM generation compiler returned: 0
<source>: In function 'int main()':
<source>:4:15: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
    4 |     char* str{"hello world!"};  // 将字符串字面量赋值给 char* 类型变量
      |               ^~~~~~~~~~~~~~
Execution build compiler returned: 0

程序运行出错信息:

Program returned: 139
Program terminated with signal: SIGSEGV

相关解释:

当程序以信号 SIGSEGV 终止时,意味着程序试图访问未分配给它的内存,或者试图访问一个不属于它的内存地址。

SIGSEGV 是“Segmentation Violation”的缩写,是指“段错误”(Segmentation Fault),表明程序访问了无效的内存地址。

原始字符串字面量

是什么?为什么?怎么用?

原始字符串字面量是可以跨越多行代码的字面量,不需要转义嵌入的双引号,并且会将\t\n这样的文本看成普通文本,而不是解释成转移序列。

#include <iostream>

int main() {
    {
        // 字符串字面量 和 原始字符串字面量 对比

        // 双引号
        std::cout << "hello, my name is \"Aoyu\"." << std::endl;
        std::cout << R"(hello, my name is "Aoyu".)" << std::endl;

        // 转义序列
        std::cout << "hello,\nworld!" << std::endl;
        std::cout << R"(hello,\nworld!)" << std::endl;
        std::cout << R"(hello,
world!)" << std::endl;
    }

    {
        // 由于 原始字符串字面量 以 )" 结尾,所以无法在字符串中包含 )"
        // 解决办法:使用扩展的原始字符串字面量语法(extended raw string literal syntax)
        std::cout << R"==("Person(Aoyu)")==" << std::endl;
        std::cout << R"-("Person(Aoyu)")-" << std::endl;
        // 上面在()两侧的 == 或 -,称为 定界序列 delimiter sequence
        // 可以任意选择,只要没在字符串中出现就可以,最长可以有 16 个字符
    }

    return 0;
}

std::string 字符串字面量

C++风格字符串,std:string 类