【C++】C++基础记录(三)

【C++】C++基础记录(三)

本文记录了我学习C++的一些基础条目知识

本文是C++扫盲的第三篇记录,从STL位标志往后的一些进阶内容。

前面的相关文章:

C++基础记录

C++基础记录(二)

【算法刷题】C++常见容器使用集合

STL位标志

bitset

std::bitset 是 C++ 标准库中一个非常实用的类,它提供了一种管理和操作固定大小的位的集合(bit set)的方式。你可以把它想象成一个数组,但这个数组的每个元素都只能是 0 或 1。

从低位到高位是从右往左数,第 0 位是最右边的位。

主要特点:

  • 固定大小: std::bitset 的大小在编译时就确定了。你需要用一个模板参数来指定它能存储多少位,例如 std::bitset<32> 表示一个能存储 32 位的 bit set。
  • 高效: 因为大小是固定的,它在内存中通常会以一个或多个无符号整数的形式存储,这使得位操作(如位移、按位与、或、异或等)非常高效。
  • 方便的接口: 它提供了很多方便的方法来操作和查询位,比如设置、清零、翻转、测试特定位等。

实例化 std::bitset

实例化这个模板类时,必须通过一个模板参数指定实例需要管理的位数:

bitset <4> fourBits; // 4 bits initialized to 0000

还可将 bitset 初始化为一个用字符串字面量(char*)表示的位序列:

bitset <5> fiveBits("10101"); // 5 bits 10101

使用一个 bitset 来实例化另一个 bitset 非常简单:

bitset <8> fiveBitsCopy(fiveBits); 

使用实例:

#include <iostream>
#include <bitset>
using namespace std;
int main() {
  // 创建一个 4 位的 bitset
  bitset<4> fourBits; // 4 bits initialized to 0000
  // 创建一个 5 位的 bitset
  bitset<5> fiveBits("10101"); // 5 bits 10101
  // 创建一个 8 位的 bitset
  bitset<8> eightBits(255); // 8 bits 11111111
  // 创建一个 8 位的 bitset 副本
  bitset <8> eightBitsCopy(eightBits); 
  
  return 0;
}

bitset运算符

std::bitset 提供了多种运算符来进行位操作,这些运算符使得对位集合的操作变得非常直观和高效。以下是一些常用的运算符:

运算符描述
«将位序列的文本表示插入到输出流中
»将一个字符串插入到bitset对象中
~按位取反
&按位与
|按位或
^按位异或
«=左移,例如左移两位 fourBits «= 2;
»=右移
[]访问特定位

使用举例:

#include <iostream>
#include <bitset>

using namespace std;

int main() {
    bitset<8> b1(1);
    // 00000001
    b1 <<= 2;
    // 左移两位,变为 00000100
    cout << "b1 after left shift by 2: " << b1 << endl;
    return 0;
}

取反:

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    bitset<8> b1(5); // 00000101
    cout << "b1: " << b1 << endl;
    cout << "b1 after NOT: " << ~b1 << endl; // 11111010
    return 0;
}

bitset成员方法

| 函数 | 描述 | | — | — | | set() | 将序列中的所有位都设置为 1 | | set (N, val=1) | 将第 N+1 位设置为 val 指定的值(默认为 1) | | reset() | 将序列中的所有位都重置为 0 | | reset (N) | 将偏移位置为(N+1)的位清除 | | flip() | 将位序列中的所有位取反 | | size() | 返回序列中的位数 | | count() | 返回序列中值为 1 的位数 |

使用举例:

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    bitset<8> b1(5); // 00000101
    cout << "b1: " << b1 << endl;
    b1.set(2); // 将第 3 位设置为 1
    cout << "b1 after set(2): " << b1 << endl;
    b1.reset(2); // 将第 3 位设置为 0
    cout << "b1 after reset(2): " << b1 << endl;
    b1.flip(); // 取反
    cout << "b1 after flip(): " << b1 << endl;
    return 0;
}

vector<bool>

STL bitset 的缺点之一是不能动态地调整长度。仅当在编辑阶段知道序列将存储多少位时才能使用 bitset。 为了克服这种缺点,STL 向程序员提供了 vector<bool> 类(在有些 STL 实现中为bit_vector)。

实例化 vector<bool> 的方式与实例化 vector 类似,有一些方便的重载构造函数可供使用:

vector <bool> boolFlags1;

例如,可创建一个这样的 vector,即它最初包含 10 个布尔元素,且每个元素都被初始化为 1(即true):

vector <bool> boolFlags2 (10, true);

还可使用一个 vector<bool> 创建另一个 vector<bool>

vector <bool> boolFlags2Copy (boolFlags2); 

vector<bool> 成员方法和运算符

vector<bool>提供了函数 flip(),用于将序列中的布尔值取反,这与函数 bitset<>::flip() 很像。

除这个方法外,vector<bool>std::vector 极其相似,例如,可使用 push_back 将标志位插入到序列中。

使用举例:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<bool> vb(3);
    vb[0] = true;
    vb[1] = false;
    vb[2] = true;
    vb.push_back(true);
    
    for (int i = 0; i < vb.size(); i++)
    {
        cout << vb[i] << endl;
    }

    vb.flip();
    cout << "After flip:" << endl;
    for (int i = 0; i < vb.size(); i++)
    {
        cout << vb[i] << endl;
    }

    return 0;
}

一开始写作了:

vector<bool> vb;
vb[0] = true;

没有指定大小,就直接使用 0 位,严重错误,直接越界。 需要养成声明即初始化的好习惯 ,使用前确定大小。

可以先初始化大小,或者先push_back添加元素,再修改对应位置。也可以使用初始化列表: vector <bool> boolFlags{ true, true, false } .

智能指针

C++在内存分配、释放和管理方面具有其他语言不具备的灵活性。同时这种松散的机制也会引发一些不确定性。

例如:

SomeClass* ptrData = anObject.GetData ();
/*
 Questions: Is object pointed by ptrData dynamically allocated using new?
 If so, who calls delete? Caller or the called?
 Answer: No idea!
*/
ptrData->DoSomething();

在上述代码中,没有显而易见的方法获悉 ptrData 指向的内存:

  • 是否是从堆中分配的,因此最终需要释放;
  • 是否由调用者负责释放;
  • 对象的析构函数是否会自动销毁该对象。

优势

智能指针可以自动管理动态分配的内存,避免内存泄漏和悬空指针等问题。

smart_pointer<SomeClass> spData = anObject.GetData ();
// Use a smart pointer like a conventional pointer!
spData->Display ();
(*spData).Display ();
// Don't have to worry about de-allocation
// (the smart pointer's destructor does it for you)

智能指针的行为类似常规指针(这里将其称为原始指针),但通过重载的运算符和析构函数确保动态分配的数据能够及时地销毁,从而提供了更多有用的功能。

智能指针类重载了 解除引用运算符(*)成员选择运算符(->) ,让程序员可以像使用常规指针那样使用智能指针。

一个简单的智能指针类

代码如下:

template <typename T>
class smart_pointer
{
  private:
    T* ptr;
  public:
    smart_pointer(T* p = nullptr) : ptr(p) {}
    ~smart_pointer() { delete ptr; }
    T& operator*() { return *ptr; }
    T* operator->() { return ptr; }
    // copy constructor
    smart_pointer(const smart_pointer& sp) : ptr(sp.ptr) {}
    // assignment operator
    smart_pointer& operator=(const smart_pointer& sp)
    {
        if (this != &sp)
        {
            delete ptr;
            ptr = sp.ptr;
        }
        return *this;
    }
};

实现了 *-> 运算符,从而可以像常规意义上的指针那样使用它。

插入:内存管理策略

使智能指针真正“智能”的是复制构造函数、赋值运算符和析构函数的实现,它们决定了智能指针对象被传递给函数、赋值或离开作用域(即像其他类对象一样被销毁)时的行为。

策略:深复制

回顾一下切除问题:

void MakeFishSwim(Fish aFish)
{
    aFish.swim();
}

...

Carp carp1;
MakeFishSwim(carp1);
// Slicing: only the Fish part of Carp is sent to MakeFishSwim() 

Tuna tuna1;
MakeFishSwim(tuna1);

下面实例,使用基于深复制的智能指针将多态对象作为基类对象进行传递:

template <typename T>
class deepcopy_smart_ptr
{
  private:
    T* ptr;
    // copy constructor
    deepcopy_smart_ptr(const deepcopy_smart_ptr& source)
    {
      ptr = source->Clone();
    }
    // assignment operator
    deepcopy_smart_ptr& operator=(const deepcopy_smart_ptr& source)
    {
        if (ptr)
        {
            delete ptr;
        }
        ptr = source->Clone();
        return *this;
    }
};

实现了一个复制构造函数,使得能够通过函数 Clone() 函数对多态对象进行深复制—类必须实现函数 Clone() 。另外,它还实现了复制赋值运算符,为了简单起见,这里假设基类 Fish 实现的虚函数为 Clone() 。通常,实现深复制模型的智能指针通过模板参数或函数对象提供该函数。下面是 deepcopy_smart_ptr 的一种用法:

deepcopy_smart_ptr<Carp> freshWaterFish(new Carp);
MakeFishSwim (freshWaterFish); // Carp will not be 'sliced' 

策略:写时复制(Copy on Write)

COW是一种优化策略,它推迟了资源的复制操作。当你有两个对象共享同一份数据时,COW会让你在第一次修改数据时才真正地去复制它。

COW的核心思想

想象一下,你有一个std::string对象,叫s1,里面存着一段很长的文本。现在你想要用s1去初始化另一个std::string对象s2

std::string s1 = "这是一段很长的文本...";
std::string s2 = s1; // 理论上s2是s1的一个拷贝

如果std::string使用了COW,那么在这一步,s2并不会立刻复制s1的数据。相反,s1s2会共享同一份底层数据。为了实现这一点,通常会有一个引用计数(reference count)来记录有多少个对象正在共享这份数据。在这个例子中,这份数据的引用计数会从1增加到2。

什么时候会触发复制?

复制操作只会在你试图修改其中一个对象时发生。比如,你想修改s2

s2 += ",后面又加了一些新内容。"; // 触发写时复制

在你执行这行代码时,系统会检查s2所指向的数据的引用计数。因为它大于1,所以系统会:

  1. s2分配一块新的内存。
  2. 将原始数据(“这是一段很长的文本…”)从旧地址复制到新地址。
  3. s2指向这份新数据。
  4. 将原始数据的引用计数减1。
  5. 最后,将新内容添加到s2的新数据中。
智能指针的COW应用

写时复制机制(Copy on Write,COW)试图对深复制智能指针的性能进行优化,它共享指针,直到首次写入对象。首次调用非 const 函数时,COW 指针通常为该非 const 函数操作的对象创建一个副本,而其他指针实例仍共享源对象。

COW 深受很多程序员的喜欢。实现 const 和非 const 版本的运算符*和->,是实现 COW 指针功能的关键。非 const 版本用于创建副本。

重要的是,选择 COW 指针时,在使用这样的实现前务必理解其实现细节。否则,复制时将出现复制得太少或太多的情况。

策略:引用计数智能指针

引用计数智能指针,通常指的是 C++ 标准库中的 std::shared_ptr ,它是一种用于管理动态分配对象生命周期的智能指针。它的核心思想是共享所有权(shared ownership),即多个智能指针可以同时指向同一个对象,并且该对象只有 在所有指向它的智能指针都被销毁或重置后,才会自动释放内存。因此,引用计数提供了一种优良的机制,使得可共享对象而无法对其进行复制。

这种智能指针被复制时,需要将对象的引用计数加 1。至少有两种常用的方法来跟踪计数:

  • 在对象中维护引用计数;
  • 引用计数由共享对象中的指针类维护。

前者称为入侵式引用计数,因为需要修改对象以维护和递增引用计数,并将其提供给管理对象的智能指针。COM 采取的就是这种方法。

后者是智能指针类将计数保存在自由存储区(如动态分配的整型),复制时复制构造函数将这个值加 1。

因此,使用引用计数机制,程序员只应通过智能指针来处理对象。在使用智能指针管理对象的同时让原始指针指向它是一种糟糕的做法,因为智能指针将在它维护的引用计数减为零时释放对象,而原始指针将继续指向已不属于当前应用程序的内存。

引用计数还有一个独特的问题:如果两个对象分别存储指向对方的指针,这两个对象将永远不会被释放,因为它们的生命周期依赖性导致其引用计数最少为 1。即循环引用。

循环引用的解决办法

为了解决循环引用的问题,C++ 提供了std::weak_ptr。std::weak_ptr 是一种不增加引用计数的智能指针,它通常用于打破循环引用,或者观察一个对象而不会阻止其被销毁。当你需要访问 std::weak_ptr 所指向的对象时,你需要先将其转换为 std::shared_ptr

区分于 std::shared_ptrstd::weak_ptr 不会增加对象的强引用计数。对应的,被 weak_ptr 指向的引用,成为弱引用。只要有一个 std::weak_ptrstd::shared_ptr 指向控制块,弱引用计数就大于 0。当弱引用计数降为 0 时,控制块才会被销毁。

std::weak_ptr 只会影响弱引用计数,而不会影响强引用计数。这意味着,即使有多个 std::weak_ptr 指向同一个对象,只要没有 std::shared_ptr 指向它,对象随时都可能被销毁。

核心功能:lock() 方法

std::weak_ptr 最大的特点是它不能直接访问所指向的对象。为了安全地使用它,你需要先调用 lock() 方法。

lock() 方法会检查对象是否仍然存在(即强引用计数是否大于 0)。

  • 如果对象存在,lock() 会返回一个临时的 std::shared_ptr。这时,强引用计数会增加 1,确保在你使用这个临时智能指针期间,对象不会被销毁。使用完毕后,这个临时智能指针会自动销毁,引用计数会减 1。
  • 如果对象不存在(已经被销毁),lock() 会返回一个空的 std::shared_ptr。

这个机制非常重要,因为它保证了你在访问对象时,该对象是有效的。

策略:破坏性复制

破坏性复制是这样一种机制,即在智能指针被复制时,将对象的所有权转交给目标指针并重置原来的指针。

destructive_copy_smartptr <SampleClass> smartPtr (new SampleClass ());
SomeFunc (smartPtr); // Ownership transferred to SomeFunc
// Don't use smartPtr in the caller any more!

虽然破坏性复制机制使用起来并不直观,但它有一个优点,即可确保任何时刻只有一个活动指针指向对象。因此,它非常适合从函数返回指针以及需要利用其“破坏性”的情形。

一个破坏性复制智能指针的例子:

template <typename T>
class destructive_copy_smartptr
{
public:
    destructive_copy_smartptr(T* p = nullptr) : _ptr(p) {}
    ~destructive_copy_smartptr() { delete _ptr; }
  
    // 接收的参数并非const类型,是为了实现破坏性复制
    destructive_copy_smartptr& operator=(destructive_copy_smartptr& other)
    {
        _ptr = other._ptr;
        // 赋值后,将外部的原指针设为nullptr
        other._ptr = nullptr;
        return *this;
    }

    destructive_copy_smartptr(destructive_copy_smartptr& other)
    {
        _ptr = other._ptr;
        other._ptr = nullptr;
    }

private:
    T* _ptr;
};

不同于大多数 C++类,该智能指针类的复制构造函数和赋值运算符不能接受 const 引用,因为它在复制源引用后使其无效。这不仅不符合传统复制构造函数和赋值运算符的语义,还让智能指针类的用法不直观。复制或赋值后销毁源引用不符合预期。鉴于这种智能指针销毁源引用,这也使得它不适合用于 STL 容器,如 std::vector 或其他任何动态集合类。这些容器需要在内部复制内容,这将导致指针失效。由于种种原因,不在程序中使用破坏性复制智能指针是明智的选择。

使用 std::unique_ptr

C++标准一直支持 auto_ptr,它是一种基于破坏性复制的智能指针。C++11 终于摒弃了该智能指针,现在您应使用 std::unique_ptr

unique_ptr 是一种简单的智能指针,但其复制构造函数和赋值运算符被声明为私有的,因此不能复制它,即不能将其按值传递给函数,也不能将其赋给其他指针。

要使用 std:unique_ptr ,必须包含头文件<memory>

#include <memory> 

使用举例:

#include <iostream>
#include <memory>
using namespace std;

class Fish
{
  public:
    Fish() { cout << "Fish constructor" << endl; }
    ~Fish() { cout << "Fish destructor" << endl; }
    void Swim() { cout << "Fish swim" << endl; }
};

void MakeFishSwim(const unique_ptr<Fish>& inFish)
{
    inFish->Swim();
}

int main()
{
    unique_ptr<Fish> pFish(new Fish);
    pFish->Swim();

    MakeFishSwim(pFish);

    unique_ptr<Fish> pFish2;
    // error: operator= is private
    // pFish2 = pFish;

    return 0;
}

可以看到,pFish指向的对象是在main函数中创建的,当main函数结束时,pFish指向的对象会自动销毁,无需手动调用delete。总之,unique_ptr 比 C++11 已摒弃的 auto_ptr 更安全,因为复制和赋值不会导致源智能指针对象无 效。它在销毁时释放对象。

unique_ptr 支持移动语义,即可以将一个 unique_ptr 移动到另一个 unique_ptr 中。移动语义可以避免复制对象,提高效率。

移动语义的使用举例:

#include <iostream>
using namespace std;

int main()
{
  unique_ptr<int> ptr1(new int(1));
  cout << *ptr1 << endl;
  cout << ptr1.get() << endl;

  unique_ptr<int> ptr2 = move(ptr1);
  cout << *ptr2 << endl;
  cout << ptr2.get() << endl;

  // 移动后,ptr1 不再指向原对象,而是由ptr2指向这个对象
  // ptr1 指向 nullptr
  if(ptr1)
  {
    cout << *ptr1 << endl;
  } 
  else
  {
    cout << "ptr1 is null" << endl;
  }

  return 0;
}

深受欢迎的三方智能指针库

显然,C++标准库提供的智能指针并不能满足所有程序员的需求,这就是还有很多其他智能指针库的原因。

Boost 提供了一些经过测试且文档完善的智能指针类,还有很多其他的实用类。

有关 Boost 智能指针的更详细信息,请访问 boost smart_ptr,在这里还可下载相关的库。

使用流进行输入和输出

C++ 的流(Streams)是处理输入/输出(I/O)的核心机制。它抽象了数据源和数据目的地,将数据的读取和写入操作统一起来,使得开发者可以用同样的方式处理来自不同地方的数据,比如文件、键盘、屏幕、网络等。

你可以把“流”想象成一条水管:

  • 输入流(Input Stream):数据从源头流向你的程序,就像水从水源流进水管一样。
  • 输出流(Output Stream):数据从你的程序流向目的地,就像水从水管流出去一样。

流类库的核心概念

C++ 标准库中的流类库(通常称为 iostream)主要由几个基类组成,它们共同构成了流处理的基础:

  1. std::istream (输入流)

    • 处理从外部设备读取数据的操作。
    • 例如,从键盘读取数据用到的 std::cin 就是 std::istream 的一个实例。
    • 常用的操作符是提取操作符 >>,例如 std::cin >> myVar;
  2. std::ostream (输出流)

    • 处理向外部设备写入数据的操作。
    • 例如,向屏幕输出数据用到的 std::cout 就是 std::ostream 的一个实例。
    • 常用的操作符是插入操作符 <<,例如 std::cout << "Hello, world!";
  3. std::iostream (输入/输出流)

    • 同时支持输入和输出,通常用于处理可以双向通信的设备。
    • 例如,文件流 std::fstream 就继承自这个类。

重要的流类和流对象

| 类/对象 | 用途 | | — | — | | cout | 标准输出流,通常被重定向到控制台 | | cin | 标准输入流,通常用于将数据读入变量 | | cerr | 用于显示错误信息的标准输出流 | | fstream | 用于操作文件的输入和输出流,继承了 ofstream 和 ifstream | | ofstream | 用于操作文件的输出流类,即用于创建文件 | | ifstream | 用于操作文件的输入流类,即用于读取文件 | | stringstream | 用于操作字符串的输入和输出流类,继承了 istringstream 和 ostringstream,通常用于在字符串和其他类型之间进行转换 |

cout、cin 和 cerr 分别是流类 ostream、istream 和 ostream 的全局对象。由于是全局对象,它们在 main( )开始之前就已初始化。

使用流类时,可指定为您执行特定操作的控制符(manipulator)。std::endl 就是一个这样的控制符,您一直在使用它来插入换行符:

std::cout << "This lines ends here" << std::endl;

std命名空间常用于流的控制符

| 控制符 | 用途 | | — | — | | 输出控制符 | | endl | 插入一个换行符 | | ends | 插入一个空字符 | | 基数控制符 | | dec | 让流以十进制方式解释输入或显示输出 | | hex | 让流以十六进制方式解释输入或显示输出 | | oct | 让流以八进制方式解释输入或显示输出 | | 浮点数表示控制符 | | fixed | 让流以定点表示法显示数据 | | scientific | 让流以科学表示法显示数据 | | <iomanip> 控制符 | | setprecision | 设置小数精度 | | setw | 设置字段宽度 | | setfill | 设置填充字符 | | setbase | 设置基数,与使用 dec、hex 或 oct 等效 | | setiosflag | 通过类型为 std::ios_base::fmtflags 的掩码输入参数设置标志 | | resetiosflag | 将 std::ios_base::fmtflags 参数指定的标志重置为默认值 |

std::cout 指定格式写入控制台

修改数字显示格式

可以让 cout 以十六进制或八进制方式显示整数。

#include <iostream>
using namespace std;

int main()
{
  int num = 255;

  cout << "Decimal: " << dec << num << endl;
  cout << "Hexadecimal: " << hex << num << endl;
  cout << "Octal: " << oct << num << endl;

  return 0;
}

setiosflags 是 C++ <iomanip> 头文件中的一个函数,主要用于设置输出流的格式标志。这些格式标志决定了数据在输出时的显示方式,比如对齐方式、数字基数、是否显示正负号等。

对上面的代码进一步使用 setiosflags 函数,打印大写十六进制字母。

#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
  int num = 255;

  cout << "Integer in hex using base notation: ";
  cout << setiosflags(ios_base::hex|ios_base::showbase|ios_base::uppercase); 

  cout << "Hexadecimal: " << num << endl;

  return 0;
}

在 cout « 链式输出中,setiosflags 的作用范围没有生效。根本原因在于 setiosflags 操纵符是在它所在的流(cout)上设置格式标志,但这个设置并不会立即影响到它后面的第一个字符串字面量 “Hexadecimal: “。cout « setiosflags(…) 这条语句会先执行,把 cout 的输出格式设置为十六进制、显示基数前缀并使用大写字母。但是,紧接着的 cout « “Hexadecimal: “ 是一个字符串,它不受这些数字格式的影响。当 cout 遇到下一个可以被格式化的数据,也就是你的整数变量 num 时,cout 的输出格式已经被重置了。

写作:

cout << "Integer in hex using base notation: " << setiosflags(ios_base::hex|ios_base::showbase|ios_base::uppercase) << num << endl;

仍然失效,暂不清楚原因,但是有一个更现代的写法是可以生效的:

cout << "Hexadecimal: " << hex << showbase << uppercase << num << endl;

另一个例子,使用 cout 以定点表示法和科学表示法显示 Pi 和圆面积:

#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
  const double pi = (double) 22.0 / 7.0;
  double radius = 5.0;
  double area = pi * radius * radius;

  cout << fixed << setprecision(7);
  cout << "Pi: " << pi << endl;
  cout << scientific << "Scientific: " << pi <<endl;
  
  cout << "Area: " << area << endl;

  return 0;
}

result:

Pi: 3.1428571
Scientific: 3.1428571e+00
Area: 7.8571429e+01

使用 std::cout 对齐文本和设置字段宽度

可使用 setw() 控制符来设置字段宽度,插入到流中的内容将在指定宽度内右对齐。在这种情况下,还可使用 setfill() 指定使用什么字符来填充空白区域。

#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
  cout << endl;
  cout << setw(30) << "Hello" << endl;
  cout << setw(10) << setfill('*') << "World" << endl;
  return 0;
}

使用 std::cin 进行输入

使用 std::cin 将输入读取到基本类型变量中

std::cin 用途广泛,让您能够将输入读取到基本类型(如 int、double 和 char*)变量中。您还可使用 getline() 从键盘读取一行输入。

使用实例:

#include <iostream>
using namespace std;

int main()
{
  int num;
  cout << "Enter an integer: ";
  cin >> num;
  cout << "You entered: " << num << endl;

  double pi;
  cout << "Enter the value of Pi: ";
  cin >> pi;
  cout << "You entered: " << pi << endl;

  char ch1, ch2, ch3;
  cout << "Enter three characters separated by spaces: ";
  cin >> ch1 >> ch2 >> ch3;
  cout << "You entered: " << ch1 << ch2 << ch3 << endl;

  return 0;
}

使用cin::get安全地读取字符

cin 让您能够将输入直接写入 int 变量,也可将输入直接写入 char 数组(C 风格字符串):

cout << "Enter a line: " << endl;
char charBuf [10] = {0}; // can contain max 10 chars
cin >> charBuf; // Danger: user may enter more than 10 chars

写入 C 风格字符串缓冲区时,务必不要超越缓冲区的边界,以免导致程序崩溃或带来安全隐患,这至关重要。因此,将输入读取到 char 数组(C 风格字符串)时,下面是一种更好的方法:

cout << "Enter a line: " << endl;
char charBuf[10] = {0};
cin.get(charBuf, 9); // stop inserting at the 9th character

使用实例:

#include <iostream>
using namespace std;

int main()
{
  char charBuf[10] = {0};
  cout << "Enter something:";
  cin.get(charBuf, 9);
  cout << "You entered: " << charBuf << endl;
  return 0;
}

只要可以,就使用 std::string , 而不是C风格字符串。

使用 std::string 进行承接

按照其他类型一样的写法来接收字符串时,会有一个限制,就是遇到用户输入空格,就停止了插入。例如:

#include <iostream>
#include <string>
using namespace std;

int main()
{
  string str;
  cout << "Enter string:";
  cin >> str;
  cout << "You entered: " << str << endl;
  return 0;
}

输入“Hello world”,只接收了“Hello”,空格后面的“world”被忽略了。

要读取整行输入(包括空白),需要使用 getline( ):

#include <iostream>
#include <string>
using namespace std;

int main()
{
  string str;
  cout << "Enter your name:";
  getline(cin, str);
  cout << "You entered: " << str << endl;
  return 0;
}

使用 std::fstream 进行文件操作

要使用 std::fstream 类或其基类,需要包含头文件 <fstream>

#include <fstream> 

要使用 fstream、ofstream 或 ifstream 类,需要使用方法 open( )打开文件:

fstream myFile;
myFile.open("HelloFile.txt",ios_base::in|ios_base::out|ios_base::trunc);
if (myFile.is_open()) // check if open() succeeded
{
 // do reading or writing here
 myFile.close();
}

open() 接受两个参数:第一个是要打开的文件的路径和名称(如果没有提供路径,将假定为应用程序的当前目录设置);第二个是文件的打开模式。在上述代码中,指定了模式 ios_base::trunc(即便指定的文件存在,也重新创建它)、ios_base::in(可读取文件)和 ios_base::out(可写入文件)。

注意到在上述代码中使用了 is_open( ),它检测 open( )是否成功。

保存到文件时,必须使用 close() 关闭文件流。无论是使用构造函数还是成员方法 open() 来打开文件流,都建议您在使用文件流对象前,使用 is_open()检查文件打开操作是否成功。

还有另一种打开文件流的方式,那就是使用构造函数:

fstream myFile("HelloFile.txt",ios_base::in|ios_base::out|ios_base::trunc);

如果只想打开文件进行写入,可使用如下代码:

ofstream myFile("HelloFile.txt", ios_base::out);

如果只想打开文件进行读取,可使用如下代码:

ifstream myFile("HelloFile.txt", ios_base::in); 

可在下述各种模式下打开文件流。

  • ios_base::app:附加到现有文件末尾,而不是覆盖它。
  • ios_base::ate:切换到文件末尾,但可在文件的任何地方写入数据。
  • ios_base::trunc:导致现有文件被覆盖,这是默认设置。
  • ios_base::binary:创建二进制文件(默认为文本文件)。
  • ios_base::in:以只读方式打开文件。
  • ios_base::out:以只写方式打开文件。

打开文件并使用«写入内容

代码示例:

#include <iostream>
#include <string>
#include <fstream>
using namespace std;

int main()
{
  // 使用open打开一个文件,输入内容
  ofstream myFile;

  myFile.open("test.txt", ios_base::out);

  myFile << "Open File Success" << endl;

  myFile << "hello world" << endl;

  myFile.close();

  return 0;
}

打开文件并使用»读取内容

代码示例:

#include <iostream>
#include <string>
#include <fstream>
using namespace std;

int main()
{
  // 使用open打开一个文件,输入内容
  ifstream myFile;

  myFile.open("test.txt", ios_base::in);
  
  if(myFile.is_open())
  {
    cout << "Open File Success" << endl;
    string content;
    while(myFile.good())
    {
      getline(myFile, content);
      cout << content << endl;
    }
    myFile.close();
  }
  else
  {
    cout << "Open File Failed" << endl;
  }

  return 0;
}

ifstream的good函数什么作用

ifstream 的 good() 函数用于检查输入流的当前状态,以确定它是否可以继续进行有效的输入/输出操作。

good() 函数的作用,good() 函数会返回一个布尔值:

  • true:如果流没有设置任何错误标志。这意味着文件流处于“正常”状态,没有遇到文件结束、读取失败或格式错误等问题。你可以安全地继续读取数据。
  • false:如果流的以下任何一个或多个错误标志被设置了:
    • failbit: 发生了非致命的 I/O 错误。例如,你试图将一个非数字字符读入一个整数变量。
    • eofbit: 达到了文件末尾 (End-Of-File)。这意味着已经没有更多数据可供读取。
    • badbit: 发生了致命的 I/O 错误。例如,磁盘读取错误。

读写二进制文件

写入二进制文件的流程与前面介绍的流程差别不大,重要的是在打开文件时使用 ios_base::binary 标志。通常使用 ofstream::writeifstream::read 来读写二进制文件。

将一个结构写入二进制文件,并且使用该文件恢复出一个结构:

#include <fstream>
#include <iostream>

using namespace std;

struct Student {
    int id;
    char name[20];
    int age;
};

int main() {
    Student s1 = {1, "zhangsan", 18};
    ofstream myFile("test.bin", ios_base::out | ios_base::binary);
    if (myFile.is_open()) {
        myFile.write(reinterpret_cast<const char *>(&s1), sizeof(Student));
        myFile.close();
    }
    Student s2;
    ifstream myFile2("test.bin", ios_base::in | ios_base::binary);
    if (myFile2.is_open()) {
        myFile2.read((char *)&s2, sizeof(Student));
        myFile2.close();
        cout << s2.id << endl;
        cout << s2.name << endl;
        cout << s2.age << endl;
    }
    return 0;
}
  • 使用了 ifstream::readofstream::write 来读写文件;
  • 使用了 reinterpret_cast 来将结构转换为字符指针。和下面的强转效果是一样的;

结构化的数据存储到 XML 文件中是更好的选择。XML 是一种基于文本和标记的存储格式,在持久化信息方面提供了灵活性和可扩展性。发布这个程序后,如果您对其进行升级,给结构 Human添加了新属性(如 numChildren),则需要考虑新版本使用的 ifstream::read,确保它能够正确地读取旧版本创建的二进制 数据。

使用 std::stringstream 对字符串进行转换

假设您有一个字符串,它包含字符串值 45,如何将其转换为整型值 45 呢?如何将整型值 45 转换为字符串 45 呢?C++提供的 stringstream 类是最有用的工具之一,让您能够执行众多的转换操作。

要使用 std::stringstream 类,需要包含头文件 <sstream>

#include <sstream> 

使用实例:

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

using namespace std;

int main() {
    cout << "Enter an integer: " << endl;
    int input = 0;
    cin >> input;

    stringstream converterStream;
    converterStream << input;

    string inputStr;
    converterStream >> inputStr;

    cout << "You entered: " << inputStr << endl;

    stringstream anotherStream;
    anotherStream << inputStr;
    int anotherInput = 0;
    anotherStream >> anotherInput;

    cout << "The integer value is: " << anotherInput << endl;

    return 0;
}

该程序让用户输入一个整型值,并使用运算符 << 将其插入到一个 stringstream 对象中。然后,您使用提取运算符将这个整数转换为 string。接下来,您将存储在 inputAsStr 中的字符串转换为整数,并将其存储到 Copy 中。

流小结

  • 只想读取文件时,务必使用 ifstream。
  • 只想写入文件时,务必使用 ofstream。
  • 插入文件流或从文件流中提取之前,务必使用 is_open() 核实是否成功地打开了它。
  • 使用完文件流后,别忘了使用方法 close() 将其关闭。
  • 别忘了,使用代码 cin»strData;从 cin 提取内容到 string 中时,通常导致 strData 只包含空白前的文本,而不是整行。
  • 别忘了,函数 getline(cin, strData); 从输入流中获取整行,其中包括空白。

异常处理

现实世界千差万别,没有两台计算机是相同的,即便硬件配置一样。这是因为在特定时间,可用的资源量取决于计算机运行的软件及其状态,因此即便在开发环境中内存分配完美无缺,在其他环境中也可能出问题。

这些问题导致了异常。异常会打断应用程序的正常流程。毕竟,如果没有内存可用,应用程序就无法完成分配给它的任务。然而,应用程序可处理这种异常:向用户显示一条友好的错误消息、采取必要的挽救措施并妥善地退出。

异常可能是外部因素导致的,如系统没有足够的内存;也可能是应用程序内部因素导致的,如使用的指针包含无效值或除数为零。为了向调用者指出错误,有些模块引发异常。

通过对异常进行处理,有助于避免出现“访问违规”和“未处理的异常”等屏幕,还可避免收到相关的抱怨邮件。下面来看看 C++都向您提供了哪些应对意外的工具。

使用try catch 处理异常

在捕获异常方面, trycatch 是最重要的 C++关键字。要捕获语句可能引发的异常,可将它们放在 try 块中,并使用 catch 块对 try 块可能引发的异常进行处理:

void SomeFunc() {
    try {
        int* numPtr = new int;
        *numPtr = 999;
        delete numPtr;
    } catch (...)  // ... catches all exceptions
    {
        cout << "Exception in SomeFunc(), quitting" << endl;
    }
}

使用举例,用户输入为 -1 个整数预留空间:

#include <iostream>
using namespace std;

int main() {
    try {
        cout << "Enter the size of the array: " << endl;
        int size = 0;
        cin >> size;
        int* numPtr = new int[size];
        cout << "the array size is: " << size << endl;
    } catch (...) {
        cout << "Exception in main(), quitting" << endl;
    }
    return 0;
}

使用 catch(...) 可以捕获所有异常。在这个场景下,也可以指定 const std::bad_alloc& e 来专门捕获因为 new 失败引发的异常。

也可以先指定 const std::bad_alloc& e 来捕获异常,然后再使用 catch(...) 来捕获其他异常。确保万无一失。

#include <iostream>
using namespace std;

int main() {
    try {
        cout << "Enter the size of the array: " << endl;
        int size = 0;
        cin >> size;
        int* numPtr = new int[size];
        cout << "the array size is: " << size << endl;
    } catch (std::bad_alloc& exp) {
        cout << "Exception encountered: " << exp.what() << endl;
        cout << "Got to end, sorry!" << endl;
    } catch (...) {
        cout << "Exception in main(), quitting" << endl;
    }
    return 0;
}

输出:

Enter the size of the array: 
-1
Exception encountered: std::bad_array_new_length
Got to end, sorry!

一般而言,可根据可能出现的异常添加多个 catch( )块,这将很有帮助。

使用 throw 引发特定类型的异常

void DoSomething()
{
 if(something_unwanted)
 throw object;
} 

使用举例:

#include <iostream>
using namespace std;

int Divide(int a, int b) {
    if (b == 0) {
        throw "Division by zero";
    }
    int result = a / b;
    cout << "Result: " << result << endl;
    return result;
}

int main() {
    int a = 10;
    int b = 0;
    int result = 0;
    try {
        result = Divide(a, b);
    } catch (const char* exp) {
        cout << "Exception encountered: " << exp << endl;
    }
    return 0;
}

上述代码表明,通过捕获类型为 char* 的异常,可捕获调用函数 Divide() 可能引发的异常。另外,这里没有将整个 main() 都放在 try{ } ;中,而 只在其中包含可能引发异常的代码 。这通常是一种不错的做法,因为 异常处理也可能降低代码的执行性能

异常处理的工作流程

在程序清单 28.3 中,您在函数 Divide( ) 中引发了一个类型为 char* 的异常,并在函数 main() 中使用处理程序 catch(char*) 捕获它。

每当您使用 throw 引发异常时,编译器都将查找能够处理该异常的 catch(Type) 。异常处理逻辑首先检查引发异常的代码是否包含在 try 块中,如果是,则查找可处理这种异常的 catch(Type) 。如果 throw 语句不在 try 块内,或者没有与引发的异常兼容的 catch() ,异常处理逻辑将继续在调用函数中寻找。因此,异常处理逻辑沿调用栈向上逐个地在调用函数中寻找,直到找到可处理异常的 catch(Type) 。在退栈过程的每一步中,都将销毁当前函数的局部变量,因此这些局部变量的销毁顺序与创建顺序相反。

演示:

#include <iostream>
using namespace std;

struct StructA {
    StructA() { cout << "StructA constructor" << endl; }
    ~StructA() { cout << "StructA destructor" << endl; }
};

struct StructB {
    StructB() { cout << "StructB constructor" << endl; }
    ~StructB() { cout << "StructB destructor" << endl; }
};

void FunctionTwo() {
    StructA a;
    StructB b;
    cout << "About to Throw an Exception" << endl;
    throw "Exception in FunctionTwo()";
}

void FunctionOne() {
    try {
        StructA a;
        StructB b;
        FunctionTwo();
    } catch (const char* exp) {
        cout << "Exception encountered: " << exp << endl;
        cout << "Exception Handled in FunctionOne(), not gonna pass to Caller"
             << endl;
    }
}

int main() {
    cout << "About to call FunctionOne()" << endl;
    try {
        FunctionOne();
    } catch (const char* exp) {
        cout << "Exception encountered: " << exp << endl;
    }
    cout << "Everything alright! About to exit main()" << endl;
    return 0;
}

按照栈上的顺序,创建和销毁对象,在 FunctionOne() 中捕获了异常,就不会传递到 main() 中。

如果因出现异常而被调用的析构函数也引发异常,将导致应用程序异常终止。

std::exception类

例如捕获 std::bad_alloc 时,实际上是捕获 new 引发的 std::bad_alloc 对象。std::bad_alloc 继承了 C++标准类 std::exception,而 std::exception 是在头文件 <exception> 中声明的。

下述重要异常类都是从 std::exception 派生而来的。

  • bad_alloc:使用 new 请求内存失败时引发。
  • bad_cast:试图使用 dynamic_cast 转换错误类型(没有继承关系的类型)时引发。
  • ios_base::failure:由 iostream 库中的函数和方法引发。

std::exception 类是异常基类,它定义了虚方法 what() ;这个方法很有用且非常重要,详细地描述了导致异常的原因,让用 户知道什么地方出了问题。

由于 std::exception 是众多异常类型的基类,因此可使用 catch(const exception&) 捕获所有将 std::exception 作为基类的异常:

void SomeFunc() {
    try {
        // code made exception safe
    } catch (const std::exception& exp)  // catch bad_alloc, bad_cast, etc
    {
        cout << "Exception encountered: " << exp.what() << endl;
    }
}

从 std::exception 派生出自定义异常类

可以引发所需的任何异常。然而,让自定义异常继承 std::exception 的好处在于,现有的异常处理程序 catch(const std::exception&)不但能捕获 bad_alloc、bad_cast 等异常,还能捕获自定义异常,因为它们的基类都是 exception。

举例:

#include <iostream>
#include <string>

using namespace std;

class CustomException : public exception {
    string reason;

   public:
    CustomException(const string& reason) : reason(reason) {}

    virtual const char* what() const throw() { return reason.c_str(); }
};

int Divide(int a, int b) {
    if (b == 0) {
        throw CustomException("Divisor cannot be zero");
    }
    return a / b;
}

int main() {
    try {
        int result = Divide(10, 0);
        cout << "Result: " << result << endl;
    } catch (const CustomException& e) {
        cout << "Exception: " << e.what() << endl;
    }
    return 0;
}

请注意 CustomException::what() 的声明:

virtual const char* what() const throw()

它以 throw() 结尾,这意味着这个函数本身不会引发异常。这是对异常类的一个重要约束,如果您在该函数中包含一条 throw 语句,编译器将发出警告。如果函数以 throw(int) 结尾,意味着该函数可能引发类型为 int 的异常。

异常小结

  • 务必捕获类型为 std::exception 的异常。
  • 务必从 std::exception 派生出自定义异常类。
  • 务必谨慎地引发异常。异常不能替代返回值(如true 或 false)。
  • 不要在析构函数中引发异常。
  • 不要认为内存分配总能成功,务必将使用 new 的代码放在 try 块中,并使用 catch(std::exception&)捕获可能发生的异常。
  • 不要在catch( )块中包含实现逻辑或分配资源的代码,以免在处理异常的同时导致异常。
- 问:为何引发异常,而不是返回错误?
- 答:不是什么时候都可以返回错误。如果调用 new 失败,需要处理 new 引发的异常,以免应用程序崩溃。另外,如果错误非常严重,导致应用程序无法正常运行,应考虑引发异常。

- 问:为何自定义异常类应继承 std::exception?
- 答:当然,并非必须这样做,但这让您能够重用捕获 std::exception 异常的所有 `catch( )` 块。编写自己的异常类时,可以不继承任何类,但必须在所有相关的地方插入新的 `catch(MyNewExceptionType&)` 语句。

- 问:我编写的函数引发异常,必须在该函数中捕获它吗?
- 答:完全不必,只需确保调用栈中有一个函数捕获这类异常即可。

- 问:构造函数可引发异常吗?
- 答:构造函数实际上没有选择余地!它们没有返回值,指出问题的唯一途径是引发异常。

- 问:析构函数可引发异常吗?
- 答:从技术上说可以,但这是一种糟糕的做法,因为异常导致退栈时也将调用析构函数。如果因异常而调用的析构函数引发异常,将给原本就稳定并试图妥善退出的应用程序雪上加霜。

【C++】C++基础记录(二)

【C++】C++基础记录(二)

本文记录了我学习C++的一些基础条目知识

本文是C++扫盲的第二篇记录,从STL标准模板往后的一些进阶内容。

前面的相关文章:

C++基础记录

【算法刷题】C++常见容器使用集合

STL容器

STL 顺序容器 如下所示。

  • std::vector:操作与动态数组一样,在最后插入数据;可将 vector 视为书架,您可在一端添加和拿走图书。
  • std::deque:与 std::vector 类似,但允许在开头插入或删除元素。
  • std::list:操作与双向链表一样。可将它视为链条,对象被连接在一起,您可在任何位置添加或删除对象。
  • std::forward_list:类似于 std::list,但是单向链表,只能沿一个方向遍历。

STL 提供的 关联容器 如下所示。

  • std::set:存储各不相同的值,在插入时进行排序;容器的复杂度为对数。
  • std::unordered_set:存储各不相同的值,在插入时进行排序;容器的复杂度为常数。这种容器是 C++11 新增的。
  • std::map:存储键-值对,并根据唯一的键排序;容器的复杂度为对数。
  • std::unordered_map:存储键-值对,并根据唯一的键排序;容器的复杂度为对数。这种容器是C++11 新增的。
  • std::multiset:与 set 类似,但允许存储多个值相同的项,即值不需要是唯一的。
  • std::unordered_multiset:与 unordered_set 类似,但允许存储多个值相同的项,即值不需要是唯一的。这种容器是 C++11 新增的。
  • std::multimap:与 map 类似,但不要求键是唯一的。
  • std::unordered_multimap:与 unordered_map 类似,但不要求键是唯一的。这种容器是 C++11新增的。

容器适配器(Container Adapter) 是顺序容器和关联容器的变种,其功能有限,用于满足特定的需 求。主要的适配器类如下所示。

  • std::stack:以 LIFO(后进先出)的方式存储元素,让您能够在栈顶插入(压入)和删除(弹出)元素。
  • std::queue:以 FIFO(先进先出)的方式存储元素,让您能够删除最先插入的元素。
  • std::priority_queue:以特定顺序存储元素,因为优先级最高的元素总是位于队列开头。

迭代器

在C++中,迭代器(Iterator) 是一种通用概念,它提供了一种访问容器(如数组、列表、树等)中元素的方式,而无需暴露容器的底层实现细节。你可以把它想象成一个“智能指针”,它指向容器中的某个元素,并且可以向前或向后移动来遍历容器中的所有元素。

迭代器的主要作用就是将算法和容器分离。这样,你可以编写通用的算法(如排序、查找),这些算法可以应用于任何支持迭代器的容器,而不需要为每一种容器类型(如 std::vectorstd::list)重复编写相同的代码。

迭代器的核心功能

一个典型的迭代器通常会提供以下操作:

  • 解引用操作符 (*):获取迭代器当前指向的元素。
  • 自增操作符 (++):将迭代器移动到下一个元素。
  • 相等/不相等比较操作符 (==, !=):判断两个迭代器是否指向同一个位置。
  • 自减操作符 (--):将迭代器移动到上一个元素(仅限部分类型)。

STL 迭代器

STL (Standard Template Library) 迭代器 是 C++ 标准库中定义的一组特定的迭代器,它们是STL容器和算法之间的桥梁。

STL迭代器不是一个单一的类,而是一组概念和接口的集合。它们被分为五种主要类型,每种类型都有不同的功能,可以用于不同的场景:

  1. 输入迭代器 (Input Iterator)
    • 用途: 只能向前遍历容器一次,用于读取数据。
    • 例子: 输入流迭代器 (std::istream_iterator)。
  2. 输出迭代器 (Output Iterator)
    • 用途: 只能向前遍历容器一次,用于写入数据。
    • 例子: 输出流迭代器 (std::ostream_iterator)。
  3. 前向迭代器 (Forward Iterator)
    • 用途: 只能向前遍历容器,可以遍历多次。
    • 例子: std::forward_list 的迭代器。
  4. 双向迭代器 (Bidirectional Iterator)
    • 用途: 可以向前和向后遍历容器。
    • 例子: std::liststd::set 的迭代器。
  5. 随机访问迭代器 (Random Access Iterator)
    • 用途: 功能最强大,可以像指针一样进行任意位置的跳转。支持+, -, []等操作。
    • 例子: std::vector, std::string, std::deque 和 C 风格数组的迭代器。

为什么 STL 迭代器如此重要?

STL 迭代器的存在使得 STL 算法库非常强大和灵活。例如,std::sort 算法要求其参数是随机访问迭代器,因为它需要随机访问元素以进行高效的排序。而 std::find 算法只需要输入迭代器,因为它只需要从头到尾遍历一次即可。

总结来说,C++ 迭代器是一个通用的抽象概念,而 STL 迭代器是这个概念在 C++ 标准库中的具体实现,它们是连接 STL 容器和算法的通用接口,是 C++ 泛型编程的基石。

STL算法

查找、排序和反转等都是标准的编程需求,不应让程序员重复实现这样的功能。因此 STL 以 STL 算法的方式提供这些函数,通过结合使用这些函数和迭代器,程序员可对容器执行一些最常见的操作。 最常用的 STL 算法如下所示。

  • std::find:在集合中查找值。
  • std::find_if:根据用户指定的谓词在集合中查找值。
  • std::reverse:反转集合中元素的排列顺序。
  • std::remove_if:根据用户定义的谓词将元素从集合中删除。
  • std::transform:使用用户定义的变换函数对容器中的元素进行变换。 这些算法都是 std 命名空间中的模板函数,要使用它们,必须包含标准头文件<algorithm>

举例从vector中查找元素及其下标

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {10, 20, 30, 40, 50};

    // 查找元素
    int value_to_find = 30;
    auto it = std::find(vec.begin(), vec.end(), value_to_find);

    if (it != vec.end()) {
        std::cout << "Found value: " << *it << std::endl;
        // 获取下标
        int index = std::distance(vec.begin(), it);
        std::cout << "Index of value: " << index << std::endl;
    } else {
        std::cout << "Value not found." << std::endl;
    }

    return 0;
}

在 C++ 标准库中,std::distance是一个定义在 <iterator> 头文件中的函数模板,用于计算两个迭代器之间的距离。它主要用于确定从一个迭代器到另一个迭代器之间有多少个元素。

对比

字符串使用演进

C++中字符串的使用经历了从C风格字符串到C++标准库字符串类的演进,主要包括以下几个阶段:

  1. C风格字符串(C-Style Strings)
    • 这是最早的字符串表示方式,使用字符数组和空字符('\0')来表示字符串的结束。
    • 例如:char str[] = "Hello, World!";
    • 缺点:容易出现缓冲区溢出、内存管理复杂、操作不便等问题。
    • 优点:兼容性好,与C语言库函数兼容。

C++支持动态分配内存,使用newdelete运算符可以在运行时分配和释放内存。

比如使用 char * dynamicName = new char[arrayLen] 来定义一个动态分配的字符数组,其中 arrayLen 是一个整数,用于指定动态分配的字符数组的长度。

然而,如果要在运行阶段改变数组的长度,必须首先释放以前分 配给它的内存,再重新分配内存来存储数据。

如果将 char*用作类的成员属性,情况将更复杂。将对象赋给另一个对象时,如果编写正确的复制构造函数和赋值运算符,两个对象将包含同一个指针的拷贝,该指针指向相同的缓冲区。其结果是,两个对象的字符串指针存储的地址相同,指向同一个内存单元。其中一个对象被销毁时,另一个对象中的指针将非法,让应用程序面临崩溃的危险。

  1. C++标准库字符串类(std::string)
    • 这是C++标准库提供的字符串类,使用起来更加方便和安全。
    • 例如:std::string str = "Hello, World!";
    • 优点:自动管理内存、提供丰富的操作方法、支持字符串拼接、比较等。
    • 缺点:与C风格字符串相比,性能较低。

string实例化和复制

string 类提供了很多重载的构造函数,因此可以多种方式进行实例化和初始化。例如,可使用常量字符串初始化 STL string 对象或将常量字符串赋给 STL std::string 对象:

const char* constCStyleString = "Hello String!";
std::string strFromConst (constCStyleString);

或:

std::string strFromConst = constCStyleString;
// 上述代码与下面的代码类似:
std::string str2 ("Hello String!"); 

同样,可使用一个 string 对象来初始化另一个:

std::string str2Copy (str2);

可让 string 的构造函数只接受输入字符串的前 n 个字符:

// Initialize a string to the first 5 characters of another
std::string strPartialCopy (constCStyleString, 5);

还可这样初始化 string 对象,即使其包含指定数量的特定字符:

// Initialize a string object to contain 10 'a's
std::string strRepeatChars (10, 'a'); 

string元素访问

第一种:使用[]运算符

std::string str = "Hello, World!";
char ch = str[7]; // ch now holds 'W'
// 遍历
for (size_t i = 0; i < str.size(); ++i) {
    std::cout << str[i] << ' ';
}

这种方式来访问string内容时,需要注意的是,访问的下标必须在字符串的有效范围内,否则会导致未定义行为。

第二种:使用迭代器

std::string str = "Hello, World!";
// 遍历
for (std::string::iterator it = str.begin(); it != str.end(); ++it) {
    std::cout << *it << ' ';
}

这种方式更加灵活,可以方便地进行各种操作,如插入、删除等。迭代器很重要,因为很多 string 成员函数都以迭代器的方式返回其结果。

拼接string

可以使用+运算符或append方法来拼接字符串。

std::string str1 = "Hello, ";
std::string str2 = "World!";
std::string str3 = str1 + str2; // str3 now holds "Hello, World!"
std::string str1 = "Hello, ";
std::string str2 = "World!";
str1.append(str2); // str1 now holds "Hello, World!"

string中字符和子字符串的查找

可以使用find方法来查找字符或子字符串。

std::string str = "Hello, World!";
size_t pos = str.find("World"); // pos now holds 7
if (pos != std::string::npos) {
    std::cout << "Found at position: " << pos << std::endl;
} else {
    std::cout << "Not found" << std::endl;
}

find方法返回子字符串首次出现的位置,如果未找到则返回std::string::npos

在C++的 std::string 类中, string::npos 是一个静态成员常量,它的值是 string::size_type 类型所能表示的最大值。它的主要作用是作为一个 “未找到”(”not found”)的标志。当你在使用 std::string 的查找方法(如 find() 或 rfind())时,如果子字符串或字符没有被找到,这些方法就会返回 string::npos。

如果string中有不止一个的子字符串,find方法只会返回第一个子字符串的位置。如果要查找所有子字符串的位置,需要使用循环来实现。

std::string str = "Hello, World! World!";
size_t pos = str.find("World");
while (pos != std::string::npos) {
    std::cout << "Found at position: " << pos << std::endl;
    pos = str.find("World", pos + 1);
}

string的截断

如果是获取子字符串,可以使用substr方法来截取字符串的一部分。

std::string str = "Hello, World!";
std::string subStr = str.substr(7, 5); // subStr now holds "World"

如果是直接对原字符串操作,可以使用 erase() 方法来删除指定位置的字符或子字符串。

STL string 类提供了 erase()函数,具有以下用途。

  • 在给定偏移位置和字符数时删除指定数目的字符。
string sampleStr ("Hello String! Wake up to a beautiful day!");
sampleStr.erase (13, 28); // Hello String!
  • 在给定指向字符的迭代器时删除该字符。
sampleStr.erase (iCharS); // iterator points to a specific character
  • 在给定由两个迭代器指定的范围时删除该范围内的字符。
sampleStr.erase (sampleStr.begin (), sampleStr.end ()); // erase from begin

例如:

std::string str = "Hello, World!";
// 删除从位置 7 开始的 5 个字符
str.erase(7, 5); // str now holds "Hello!"

删除某一个字符:

std::string str = "Hello, World!";
auto iChar = str.find('W');
if (iChar != std::string::npos) {
    str.erase(iChar, 1); // 删除第一个 'W'
}
std::cout << str << std::endl; // 输出 "Hello, orld!"

删除指定迭代器范围内的所有字符:

std::string str = "Hello, World!";
auto iStart = str.begin() + 7;
auto iEnd = str.begin() + 12;
str.erase(iStart, iEnd); // str now holds "Hello!"

字符串反转

有时需要反转字符串的内容。假设要判断用户输入的字符串是否为回文,方法之一是将其反转,再与原来的字符串进行比较。反转 STL string 很容易,只需使用泛型算法 std::reverse() 即可:

string sampleStr ("Hello String! We will reverse you!");
reverse (sampleStr.begin (), sampleStr.end ());

std::reverse()算法根据两个输入参数指定的边界反转边界内的内容。在这里,两个边界分别是 string 对象的开头和末尾,因此整个字符串都被反转。只要提供合适的输入参数,也可将字符串的一部分反转。注意,边界不能超过 end()。

大小写转换

要对字符串进行大小写转换,可使用算法 std::transform() 来实现。

下面这个例子,将用户输入的字符串分别进行大写和小写转换:

#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>

int main() {
    std::string input;
    std::cout << "Enter a string: ";
    std::getline(std::cin, input);

    // 转换为大写
    std::string upperCaseStr = input;
    std::transform(upperCaseStr.begin(), upperCaseStr.end(), upperCaseStr.begin(), ::toupper);

    // 转换为小写
    std::string lowerCaseStr = input;
    std::transform(lowerCaseStr.begin(), lowerCaseStr.end(), lowerCaseStr.begin(), ::tolower);

    std::cout << "Uppercase: " << upperCaseStr << std::endl;
    std::cout << "Lowercase: " << lowerCaseStr << std::endl;

    return 0;
}

C++14引入的操作符””s

C++14引入了一个新的操作符""s,它可以直接将字符串字面量转换为std::string类型。这使得字符串的创建更加简洁和直观。

如果字面量字符串中包含了空字符,需要使用""s操作符来创建字符串。

#include <iostream>
#include <string>

using namespace std;

int main() {
    string str1("Hello \0 World!");
    cout << str1 << endl;
    // 输出:Hello

    string str2("Hello \0 World!"s);
    cout << str2 << endl;
    // 输出:Hello \0 World!

    return 0;
}

vector

vector 是一个模板类,提供了动态数组的通用功能,具有如下特点:

  • 在数组末尾添加元素所需的时间是固定的,即在末尾插入元素的所需时间不随数组大小而异,在末尾删除元素也如此;
  • 在数组中间添加或删除元素所需的时间与该元素后面的元素个数成正比;
  • 存储的元素数是动态的,而 vector 类负责管理内存。

vector实例化

vector是一个模板类,需要指定元素类型。

vector<int> dynamicIntArray;
vector<string> stringArray;
vector<double> doubleArray;

声明const迭代器可以这样写:

vector<int>::const_iterator iter = dynamicIntArray.cbegin();

如果需要可修改值的迭代器,需要使用iterator而不是const_iterator

vector<int>::iterator iter = dynamicIntArray.begin();

其他实例化方式:

// 初始化列表
vector<int> dynamicIntArray = {1, 2, 3, 4, 5};

// 指定大小,默认初始化元素为0
vector<int> dynamicIntArray(5);

// 指定大小,指定初始化元素,内部元素均为50
vector<int> dynamicIntArray(5, 50);

// 通过另一个vector初始化
vector<int> dynamicIntArray2(dynamicIntArray);

// 通过迭代器,使用另一个数组的一部分来初始化
vector<int> dynamicIntArray3(dynamicIntArray.begin(), dynamicIntArray.begin() + 3);

vector使用 push_back() 添加元素 和 pop_back() 删除元素

添加元素:

vector<int> dynamicIntArray;
dynamicIntArray.push_back(10);
dynamicIntArray.push_back(20);
dynamicIntArray.push_back(30);

// size() 函数返回vector中元素的个数
for (int i = 0; i < dynamicIntArray.size(); i++)
{
    cout << dynamicIntArray[i] << endl;
}

使用 pop_back() 将元素从 vector 中删除所需的时间是固定的,即不随 vector 存储的元素个数而异。例如:

dynamicIntArray.pop_back(); // 删除最后一个元素
// 现在 dynamicIntArray 只包含 10 和 20

vector使用 insert() 指定位置插入元素

有好几个重载版本:

  1. 插入指定位置,例如在开头插入一个元素:
vector<int> dynamicIntArray = {10, 20, 30};
dynamicIntArray.insert(dynamicIntArray.begin(), 5); // 在开头插入5
// 结果: 5 10 20 30
  1. 指定插入位置,元素数量,元素数值(数值相同):
vector<int> dynamicIntArray = {10, 20, 30};
dynamicIntArray.insert(dynamicIntArray.begin() + 1, 2, 15); // 在第二个位置插入两个15
// 结果: 10 15 15 20 30
  1. 将另一个vector插入指定位置:
vector<int> dynamicIntArray1 = {10, 20, 30};
vector<int> dynamicIntArray2 = {40, 50};
dynamicIntArray1.insert(dynamicIntArray1.begin() + 1, dynamicIntArray2.begin(), dynamicIntArray2.end());
// 结果: 10 40 50 20 30

vector使用数组语法访问元素

可以使用数组语法来访问 vector 中的元素:

vector<int> dynamicIntArray = {10, 20, 30};
cout << dynamicIntArray[0] << endl; // 输出 10
cout << dynamicIntArray[1] << endl; // 输出 20
cout << dynamicIntArray[2] << endl; // 输出 30

// 循环
for (int i = 0; i < dynamicIntArray.size(); i++)
{
    cout << dynamicIntArray[i] << endl;
}

vector使用at()访问元素

at() 方法提供了边界检查,如果访问越界会抛出异常。

vector<int> dynamicIntArray = {10, 20, 30};
cout << dynamicIntArray.at(0) << endl; // 输出 10
cout << dynamicIntArray.at(1) << endl; // 输出 20
cout << dynamicIntArray.at(2) << endl; // 输出 30

使用指针语法访问vector元素

可以使用迭代器以类似指针的方式访问vector元素:

vector<int> dynamicIntArray = {10, 20, 30};
vector<int>::const_iterator it = dynamicIntArray.begin();

cout << *it << endl; // 输出 10

// 遍历
for (it = dynamicIntArray.begin(); it != dynamicIntArray.end(); it++)
{
    cout << *it << endl;
}

这里使用了 ++ 运算符移动位置,使用了 * 运算符解引用迭代器。

vector的大小和容量

size() 方法返回 vector 中元素的个数,而 capacity() 方法返回 vector 分配的内存大小(即可以容纳的元素个数)。

vector<int> dynamicIntArray = {10, 20, 30};
cout << "Size: " << dynamicIntArray.size() << endl; // 输出 3
cout << "Capacity: " << dynamicIntArray.capacity() << endl; // 输出 3 或更大

如果 vector 需要频繁地给其内部动态数组重新分配内存,将对性能造成一定的影响。在很大程度上说,这种问题可以通过使用成员函数 reserve (number) 来解决。reserve 函数的功能基本上是增加分配给内部数组的内存,以免频繁地重新分配内存。通过减少重新分配内存的次数,还可减少复制对象的时间,从而提高性能,这取决于存储在 vector 中的对象类型。

vector<int> dynamicIntArray;
dynamicIntArray.reserve(100); // 预留空间以容纳100个元素
dynamicIntArray.push_back(10);
dynamicIntArray.push_back(20);
dynamicIntArray.push_back(30);
cout << "Size: " << dynamicIntArray.size() << endl; // 输出 3
cout << "Capacity: " << dynamicIntArray.capacity() << endl; // 输出 100

deque

deque(double-ended queue)是一个模板类,提供了双端队列的通用功能,除了兼顾 vector 的随机访问能力,还支持在队列的前端进行快速的插入和删除操作。

对比两种结构的底层实现:

vector和deque实现对比

C++ 的 std::vectorstd::deque 都是标准模板库(STL)中的容器,它们在底层使用了不同的数据结构来存储元素,这导致了它们在性能特性上的差异。

std::vector

std::vector 的底层实现是一个动态数组(Dynamic Array)。它的所有元素都存储在一段连续的内存块中。

  • 优点:
    • 快速随机访问: 由于内存连续,通过索引访问任何元素的时间复杂度为 $O(1)$,因为它只需要简单的指针算术运算。
    • 缓存友好: 连续的内存布局使得它在遍历元素时具有很好的 CPU 缓存局部性(cache locality),这通常能带来更好的性能。
  • 缺点:
    • 插入和删除开销大: 在数组的中间或开头插入或删除元素需要移动其后的所有元素,时间复杂度为 $O(n)$。
    • 扩容开销: 当动态数组的容量不足时,vector 需要分配一块更大的新内存,将所有旧元素复制到新内存中,然后释放旧内存。这个操作的时间复杂度也是 $O(n)$。

std::deque

和 vector 容器采用连续的线性空间不同,deque 容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于在内存的不同区域。

为了管理这些连续空间,deque 容器用数组(数组名假设为 map)存储着各个连续空间的首地址。

也就是说,map 数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间。

通过建立 map 数组,deque 容器申请的这些分段的连续空间就能实现“整体连续”的效果。

换句话说,当 deque 容器需要在头部或尾部增加存储空间时,它会申请一段新的连续空间,同时在 map 数组的开头或结尾添加指向该空间的指针,由此该空间就串接到了 deque 容器的头部或尾部。

有读者可能会问,如果 map 数组满了怎么办?很简单,再申请一块更大的连续空间供 map 数组使用,将原有数据(很多指针)拷贝到新的 map 数组中,然后释放旧的空间。

deque 容器的分段存储结构,提高了在序列两端添加或删除元素的效率,但也使该容器迭代器的底层实现变得更复杂。

deque使用 push_front()push_back()

#include <iostream>
#include <deque>
using namespace std;

int main() {
    deque<int> myDeque;

    // 在队列末尾添加元素
    myDeque.push_back(10);
    myDeque.push_back(20);
    myDeque.push_back(30);

    // 在队列前端添加元素
    myDeque.push_front(5);
    myDeque.push_front(1);

    // 当前deque内容: 1 5 10 20 30

    // 删除队列末尾的元素
    myDeque.pop_back();

    // 删除队列前端的元素
    myDeque.pop_front();

    // 修改后的deque内容: 5 10 20

    return 0;
}

deque的两种遍历方式

#include <iostream>
#include <deque>
using namespace std;

int main() {
    deque<int> myDeque = {10, 20, 30, 40, 50};

    // 使用索引遍历
    cout << "Using index:" << endl;
    for (size_t i = 0; i < myDeque.size(); ++i) {
        cout << myDeque[i] << " ";
    }
    cout << endl;

    // 使用迭代器遍历
    cout << "Using iterator:" << endl;
    for (deque<int>::iterator it = myDeque.begin(); it != myDeque.end(); ++it) {
        // 使用distance计算偏移位置
        size_t offset = distance(myDeque.begin(), it);
        cout << "Offset: " << offset << ", Value: " << *it << " ";
    }
    cout << endl;

    return 0;
}

size_t 是无符号整数类型,用于表示对象的大小或索引。

清空vector和deque

可以使用 clear() 方法清空 vectordeque,可以使用 empty() 来判断容器是否为空的。

#include <iostream>
#include <vector>
#include <deque>

using namespace std;

int main() {
    vector<int> myVector = {1, 2, 3, 4, 5};
    deque<int> myDeque = {10, 20, 30, 40, 50};

    // 清空vector
    myVector.clear();
    if (myVector.empty()) {
        cout << "Vector is empty." << endl;
    } else {
        cout << "Vector is not empty." << endl;
    }

    // 清空deque
    myDeque.clear();
    if (myDeque.empty()) {
        cout << "Deque is empty." << endl;
    } else {
        cout << "Deque is not empty." << endl;
    }

    return 0;
}
应该不应该
在不知道需要存储多少个元素时,务必使用动态数组 vector 或 deque。使用固定大小的数组,可能会浪费内存或导致溢出。
请牢记,vector 只能在一端扩容,为此可使用函数 push_back( )。试图在 vector 的前端插入元素,可能导致性能问题。
请牢记,deque 可在两端扩容,为此可使用函数 push_back( )和 push_front( )。忘记 deque 的双端特性,导致不必要的复杂操作。
访问动态数组时,不要跨越其边界。使用索引访问时不检查边界,可能导致未定义行为。
使用迭代器遍历容器,确保代码的通用性和安全性。直接使用指针操作容器,可能导致错误和不安全的代码。

别忘了,函数 pop_back() 删除集合中的最后一个元素。函数 pop_front() 删除 deque 的第一个元素。

std::list

std::list 在C++中是一个双向链表(doubly linked list),它的核心实现依赖于节点(node)和指针(pointer)。

std::list 通常使用一个 虚拟的头节点(sentinel node) 来简化操作。这个节点不存储任何实际数据,它的作用是:

  • next指针永远指向链表的第一个真实节点。
  • prev指针永远指向链表的最后一个真实节点。
  • 当链表为空时,这个虚拟头节点的next和prev都指向它自己。

使用虚拟头节点的好处是,无论是插入、删除还是遍历,你都不需要对链表为空或在头尾进行特殊判断,所有操作都可以用统一的方式处理,这大大简化了代码逻辑。

要实例化模板 list,需要指定要在其中存储的对象类型,因此实例化 list 的语法类似于下面这样:

std::list<int> linkInts; // list containing integers
std::list<float> listFloats; // list containing floats
std::list<Tuna> listTunas; // list containing objects of type Tuna 

要声明一个指向 list 中元素的迭代器,可以像下面这样做:

std::list<int>::const_iterator elementInList;

如果需要一个这样的迭代器,即可以使用它来修改值或调用非 const 函数,可将 const_iterator 替换为 iterator。

list实例化方式

#include <iostream>
#include <list>
using namespace std;
int main() {
    // 使用初始化列表
    list<int> myList = {1, 2, 3, 4, 5};

    // 使用默认构造函数
    list<string> stringList;

    // 使用指定大小和初始值
    list<double> doubleList(5, 3.14); // 包含5个3.14

    // 使用另一个list初始化
    list<int> anotherList(myList);

    // 使用vector的元素来实例化一个list
    vector<int> vec = {10, 20, 30, 40, 50};
    list<int> listFromVec(vec.cbegin(), vec.cend());

    return 0;
}

begin() 返回一个普通的可读写迭代器 (iterator)。这意味着你可以通过这个迭代器来读取或修改容器中的元素。 cbegin() 返回一个常量迭代器 (const_iterator)。这个迭代器只能用来读取容器中的元素,但不能修改它们。end同理。

您首先实例化了一个 vector,接下来,实例化了一个 list,它包含从 vector 复制而来的元素,这是使用 C++11 新增的 vector::cbegin()vector::cend() 返回的 const 迭代器复制的。该程序清单表明,迭代器让容器的实现彼此独立,其通用功能让您能够使用 vector 中的值实例化 list。

list的开头和末尾插入元素

可以使用 push_front() 方法在 list 的开头插入元素,使用 push_back() 方法在 list 的末尾插入元素。

#include <iostream>
#include <list>
using namespace std;
int main() {
    list<int> myList;

    // 在开头插入元素
    myList.push_front(10);
    myList.push_front(20);

    // 在末尾插入元素
    myList.push_back(30);
    myList.push_back(40);

    // 输出列表内容
    for (const auto& elem : myList) {
        cout << elem << " ";
    }
    cout << endl;

    return 0;
}

list中间插入元素

可以使用 insert() 方法在 list 的中间插入元素。

成员函数 list::insert()有 3 种版本。

  • 第 1 种版本:

      iterator insert(iterator pos, const T& x)
    

    在这里,insert 函数接受的第 1 个参数是插入位置,第 2 个参数是要插入的值。该函数返回一个迭代器,它指向刚插入到 list 中的元素。

  • 第 2 种版本:

      void insert(iterator pos, size_type n, const T& x)
    

    该函数的第 1 个参数是插入位置,最后一个参数是要插入的值,而第 2 个参数是要插入的元素个数。

  • 第 3 种版本:

      template <class InputIterator>
      void insert(iterator pos, InputIterator f, InputIterator l)
    

    该重载版本是一个模板函数,除一个位置参数外,它还接受两个输入迭代器,指定要将集合中相应范围内的元素插入到 list 中。注意,输入类型 InputIterator 是一种模板参数化类型,因此可指定任何集合(数组、vector 或另一个 list)的边界。

使用举例:

#include <iostream>
#include <list>
using namespace std;
int main() {
    list<int> myList = {10, 20, 30, 40};

    // 在第二个位置插入元素
    auto it = myList.begin();
    advance(it, 1); // 移动到第二个位置
    myList.insert(it, 15); // 在第二个位置插入15

    // 此时的列表内容 10 15 20 30 40

    // 在第二个位置插入多个元素
    it = myList.begin();
    advance(it, 1); // 移动到第二个位置
    myList.insert(it, 2, 25); // 在第二个位置插入2个25

    // 此时的列表内容 10 15 25 25 20 30 40

    // 在第二个位置插入数组元素
    int arr[] = {35, 45};
    it = myList.begin();
    advance(it, 1); // 移动到第二个位置
    myList.insert(it, arr, arr + 2); // 在第二个位置插入数组元素

    // 此时的列表内容 10 15 25 25 35 45 20 30 40

    return 0;
}

list删除元素

erase() 方法用于删除 list 中的元素。有两个重载版本:

  • 接受一个迭代器参数,删除指定位置的元素;
  • 接受两个迭代器参数,删除指定范围内的元素。
#include <iostream>
#include <list>
using namespace std;
int main() {
    list<int> myList = {10, 20, 30, 40, 50};

    // 删除第二个元素
    auto it = myList.begin();
    advance(it, 1); // 移动到第二个位置
    myList.erase(it); // 删除第二个元素

    // 此时的列表内容 10 30 40 50

    // 删除第二个元素到第四个元素
    it = myList.begin();
    advance(it, 1); // 移动到第二个位置
    auto endIt = myList.begin();
    advance(endIt, 3); // 移动到第四个位置
    myList.erase(it, endIt); // 删除第二个元素到第四个元素

    // 此时的列表内容: 10 50

    for (const auto& elem : myList) {
        cout << elem << " ";
    }
    cout << endl;

    return 0;
}

需要注意的是, list::erase(first, last) 删除的是 [first, last) 范围内的元素,即包含 first 指向的元素,但不包含 last 指向的元素。这也是为什么第二次删除会移除 30 和 40,而保留 50(在执行第二次删除前的列表中)。因为尾迭代器 endIt 指向的为50这个元素。

区分于it.end() ,it.end() 指向的其实是最后一个元素的下一个位置,所以使用it.end()来删除元素时,会删除到最后一个元素。

list元素反转

可以使用 reverse() 方法来反转 list 中的元素。

#include <iostream>
#include <list>
using namespace std;
int main() {
    list<int> myList = {10, 20, 30, 40, 50};

    // 反转列表元素
    myList.reverse();

    // 输出列表内容
    for (const auto& elem : myList) {
        cout << elem << " ";
    }
    cout << endl;

    return 0;
}

list进行排序

可以使用 sort() 方法对 list 中的元素进行排序。 有两个重载方法:

  • 单独一个sort()方法,默认以升序排序
  • 接受一个比较函数作为参数,以指定的标准排序
#include <iostream>
#include <list>
using namespace std;

bool SortPredicate_Descending(const int& a, const int& b) {
    return a > b; // 降序排序
}

int main() {
    list<int> myList = {40, 10, 30, 20, 50};

    // 默认升序排序
    myList.sort();

    cout << "Sorted in ascending order: ";
    for (const auto& elem : myList) {
        cout << elem << " ";
    }
    cout << endl;

    // 降序排序
    myList.sort(SortPredicate_Descending);

    cout << "Sorted in descending order: ";
    for (const auto& elem : myList) {
        cout << elem << " ";
    }
    cout << endl;

    return 0;
}

定义了函数 SortPredicate_Descending ,它是一个二元谓词,帮助 list 的 sort() 函数判断一个元素是否比另一个元素小。如果不是,则交换这两个元素的位置。换句话说,您告诉了 list 如何解释小于,就这里而言,小于的含义是第一个参数大于第二个参数。这个谓词仅在第一个值比第二个值大时返回 true。也就是说,使用该谓词时,仅当第一个元素(lsh)的数字值比第二个元素(rsh)大时,sort()才认为第一个元素比第二个元素小。基于这种解释,sort()交换元素的位置,以满足谓词指定的标准。

包含对象的list进行排序

实际使用中,很少使用list来存储int等简单内置类型,而是存储自定义的类型,这是如何排序呢?

答案是采取下面两种方式之一:

  • 在 list 包含的对象所属的类中,实现运算符<;
  • 提供一个排序二元谓词—一个这样的函数,即接受两个输入值,并返回一个布尔值,指出第一个值是否比第二个值小。
#include <iostream>
#include <list>
#include <string>

using namespace std;


class Tuna {
    int age;
    string name;
public:
    Tuna(int age, string name) {
        this->age = age;
        this->name = name;
    }
    int getAge() const {
        return age;
    }
    string getName() const {
        return name;
    }
    // 实现<操作符函数,实现按照名称name的长度升序排序
    bool operator<(const Tuna& other) const {
        return name.size() < other.name.size();
    }
};

bool SortPredicate_Age(const Tuna& a, const Tuna& b) {
    return a.getAge() < b.getAge(); // 按年龄升序排序
}

int main() {
    list<Tuna> tunaList;
    tunaList.push_back(Tuna(1, "Tunadfbdbn1"));
    tunaList.push_back(Tuna(2, "Tunafbdfbdbfsddvw2"));
    tunaList.push_back(Tuna(3, "Tuna3"));

    // 按年龄排序
    tunaList.sort(SortPredicate_Age);

    cout << "Sorted Tuna List by Age:" << endl;
    for (const auto& tuna : tunaList) {
        cout << "Name: " << tuna.getName() << ", Age: " << tuna.getAge() << endl;
    }

    tunaList.sort();
    cout << "Sorted Tuna List by < oprator:" << endl;
    for (const auto& tuna : tunaList) {
        cout << "Name: " << tuna.getName() << ", Age: " << tuna.getAge() << endl;
    }

    return 0;
}

包含对象的list进行删除

这时候需要使用list的 remove() 方法,但是需要注意的是,需要给 remove() 方法指定标准。在类中实现 == 比较运算符。

#include <iostream>
#include <list>
#include <string>
using namespace std;

class Human {
    string name;
    int age;
public:
    Human(string name, int age) {
        this->name = name;
        this->age = age;
    }
    string getName() const {
        return name;
    }
    int getAge() const {
        return age;
    }
    bool operator==(const Human& other) {
        return name == other.name;
    }
};

int main() {
    list<Human> humanList;
    humanList.push_back(Human("张三", 18));
    humanList.push_back(Human("李四", 20));
    humanList.push_back(Human("王五", 22));

    for (const auto& human : humanList) {
        cout << "Name: " << human.getName() << ", Age: " << human.getAge() << endl;
    }

    humanList.remove(Human("李四", 20));

    cout << "=======> After remove: 李四 <==========" << endl;
    for (const auto& human : humanList) {
        cout << "Name: " << human.getName() << ", Age: " << human.getAge() << endl;
    }

    return 0;
}

实现的 Human::operator==将该对象与 list 中的元素进行比较。该运算符在姓名相同时返回 true,向 list::remove( )指出了匹配标准。

std::forward_list

std::forward_list 是 C++11 引入的一个单向链表容器,提供了类似于 std::list 的功能,但只支持单向遍历。它的底层实现依赖于节点(node)和指针(pointer)。

forward_list 的用法与 list 很像,但只能沿一个方向移动迭代器,且插入元素时只能使用函数 push_front(),而不能使用 push_back()。当然,总是可以使用 insert() 及其重载版本在指定位置插入元素。

下列代码演示了 forward_list 的实例化,添加元素和单向遍历:

#include <iostream>
#include <forward_list>
using namespace std;
int main() {
    forward_list<int> myList;
    myList.push_front(1);
    myList.push_front(2);
    myList.push_front(3);
    // 现在 myList 包含 3, 2, 1
    for (const auto& elem : myList) {
        cout << elem << " ";
    }
    cout << endl;

    auto it = myList.begin();
    myList.insert_after(it, 4);
    // 现在 myList 包含 3, 4, 2, 1
    for (auto singleIt = myList.begin(); singleIt != myList.end(); singleIt++) {
        cout << *singleIt << " ";
    }
    cout << endl;

    return 0;
}

鉴于 forward_list 不支持双向迭代,因此只能对迭代器使用运算符++,而不能使用–。

列表总结:

  • 如果需要频繁地插入或删除元素(尤其是在中间插入或删除时),应使用 std::list,而不是 std::vetor。因为在这种情况下,vector 需要调整其内部缓冲区的大小,以支持数组语法,还需执行开销高昂的复制操作,而 list 只需建立或断开链接。

  • 请记住,可使用成员方法 push_front()push_back() 分别在 list 开头和末尾插入元素。

  • 对于要使用 list 等 STL 容器存储其对象的类,别忘了在其中实现运算符<和==,以提供默认的排序和删除谓词。

  • 请记住,像其他 STL 容器类一样,总是可以使用 list::size() 来确定 list 包含多少个元素。

  • 请记住,像其他 STL 容器类一样,可使用方法 list::clear() 清空 list。

  • 无需频繁在两端插入或删除元素,且不用在中间插入或删除元素时,请不要使用 list;在这些情况下,vector 和 deque 的速度要快得多。

  • 如果不想根据默认标准进行删除或排序,别忘了给 sort()remove() 提供一个谓词函数。

std::set和std::multiset

set 是 C++ 标准库中的一个关联容器,用于存储唯一的元素,自动排序。它基于红黑树实现,提供了快速的插入、删除和查找操作。

multiset 是 set 的一个变体,允许存储重复的元素。

set 的常用方法包括:

  • insert():插入元素。
  • erase():删除元素。
  • find():查找元素。
  • size():返回元素数量。
  • empty():检查是否为空。
  • clear():清空容器。

multiset 的常用方法与 set 类似,只是允许存储重复元素。

为了实现快速搜索,STL set 和 multiset 的内部结构像二叉树,这意味着将元素插入到 set 或 multiset时将对其进行排序,以提高查找速度。这还意味着不像 vector 那样可以使用其他元素替换给定位置的元素,位于 set 中特定位置的元素不能替换为值不同的新元素,这是因为 set 将把新元素同内部树中的其他元素进行比较,进而将其放在其他位置。

set和multiset的实例化

可以使用以下方式实例化:

set<int> mySet;
multiset<int> myMultiset;

set<Tuna> myTunaSet;
multiset<Tuna> myTunaMultiset;

鉴于 set 和 multiset 都是在插入时对元素进行排序的容器,如果您没有指定排序标准,它们将使用默认谓词 std::less,确保包含的元素按升序排列。

要创建二元排序谓词,可在类中定义一个 operator(),让它接受两个参数(其类型与集合存储的数据类型相同),并根据排序标准返回 true。下面是一个这样的排序谓词,它按降序排列元素:

// used as a template parameter in set / multiset instantiation
template <typename T>
struct SortDescending
{
 bool operator()(const T& lhs, const T& rhs) const
 {
     return (lhs > rhs);
 }
};

然后,在实例化 set 或 multiset 时指定该谓词,如下所示:

// a set and multiset of integers (using sort predicate)
set <int, SortDescending<int>> setInts;
multiset <int, SortDescending<int>> msetInts; 

也可以使用迭代器来通过另一个容器的元素来初始化 set 或 multiset,如下所示:

#include <iostream>
#include <set>
#include <vector>
using namespace std;

int main(){
    vector<int> vec = {1, 2, 3, 4, 5};
    set<int> setInts(vec.cbegin(), vec.cend());
    for (auto it = setInts.begin(); it != setInts.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;
    return 0;
}

set和multiset的插入元素

#include <iostream>
#include <set>
#include <vector>
using namespace std;

template <typename T>
void DisplayContents(const T& container)
{
    for (auto element = container.cbegin(); element != container.cend(); element++)
    {
        cout << *element << " ";
    }
    cout << endl;
}

int main()
{
    // 使用 insert() 方法插入单个元素
    set<int> setInts;
    setInts.insert(1);
    setInts.insert(2);
    setInts.insert(3);
    DisplayContents(setInts);

    cout << endl;

    // 使用 insert() 方法插入多个元素
    cout << "======>use another insert to add item<======" << endl;
    vector<int> vec = {4, 5, 6};
    set<int> setInts2;
    setInts2.insert(vec.cbegin(), vec.cend());
    DisplayContents(setInts2);
    cout << endl;

    return 0;
}

DisplayContents这个模板方法,用来打印一个容器里的内容。它接受一个容器作为参数,使用迭代器遍历容器中的元素,并打印每个元素。

multiset 中计算一个元素的数量有多少

使用 count() 函数可以计算 multiset 中特定元素的数量。

#include <iostream>
#include <set>
using namespace std;

int main() {
    multiset<int> myMultiset = {1, 2, 2, 2, 3, 4, 5};

    // 计算元素3的数量
    int count = myMultiset.count(2);
    cout << "Count of 2 in set: " << count << endl;   // 输出 3
    // 尝试计算一个不存在的元素
    count = myMultiset.count(6);
    cout << "Count of 6 in set: " << count << endl;  // 输出 0

    return 0;
}

set、multiset、map、multimap中查找元素

诸如 set、multiset、map 和 multimap 等关联容器都提供了成员函数 find() ,它让您能够根据给定的键来查找值:

auto elementFound = setInts.find (-1);
// Check if found...
if (elementFound != setInts.end ())
 cout << "Element " << *elementFound << " found!" << endl;
else
 cout << "Element not found in set!" << endl; 

multiset 可包含多个值相同的元素,因此对于 multiset,这个函数查找第一个与给定键匹配的元素。

#include <iostream>
#include <set>
using namespace std;

int main() {
    multiset<int> myMultiset = {1, 2, 2,3, 4, 2, 5};

    // 查找元素2
    auto it = myMultiset.find(2);
    if (it != myMultiset.end()) {
        cout << "Found element: " << *it << endl; // 输出 2
    } else {
        cout << "Element not found." << endl;
    }

    // 继续查找下一个元素2
    int count = myMultiset.count(2);
    for (int i = 0; i < count - 1; i++) {
        it++;
        cout << "Found another qualified element: " << *it << endl; // 输出 2
    }

    return 0;
}

鉴于 multiset 可能在 相邻的位置 存储多个值相同的元素,为了访问所有这些元素,可使用 find() 返回的迭代器,并将迭代器前移 count()-1 次。

set、multiset、map、multimap中删除元素

诸如 set、multiset、map 和 multimap 等关联容器都提供了成员函数 erase() ,它有三种重载方法。

  • 您能够根据键删除值:

      setObject.erase (key);
    
  • erase()函数的另一个版本接受一个迭代器作为参数,并删除该迭代器指向的元素:

      setObject.erase (element);
    
  • 通过使用迭代器指定的边界,可将指定范围内的所有元素都从 set 或 multiset 中删除:

      setObject.erase (iLowerBound, iUpperBound); 
    

使用实例:

#include <iostream>
#include <set>
using namespace std;

template <typename T>
void DisplayContents(const T& container)
{
    for (auto element = container.cbegin(); element !=container.cend(); element++)
    {
        cout << *element << " ";
    }
    cout << endl;
}

int main() {
    multiset<int> myMultiset = {1, 2, 2, 3, 4, 2, 5};

    // 删除元素2
    myMultiset.erase(2);

    // 输出删除后的内容
    cout << "After erasing 2: ";
    DisplayContents(myMultiset);

    // 删除一个不存在的元素(不会报错)
    myMultiset.erase(6); // 不存在的元素

    auto it = myMultiset.find(3);
    myMultiset.erase(it);

    // 输出删除后的内容
    cout << "After erasing 3: ";
    DisplayContents(myMultiset);

    return 0;
}

使用set实现的简要电话薄

#include <iostream>
#include <set>
#include <string>
using namespace std;

template <typename T>
void DisplayContents(const T& container)
{
    for (auto element = container.cbegin(); element !=container.cend(); element++)
    {
        cout << *element << " ";
    }
    cout << endl;
}

struct ContactItem {
    string name;
    string phone;
    string displayAs;

    ContactItem(string n, string p) : name(n), phone(p) {
        displayAs = name + "(" + phone + ")";
    }

    bool operator<(const ContactItem& other) const {
        return name < other.name;
    }

    bool operator==(const ContactItem& other) const {
        return name == other.name;
    }

    // Used in DisplayContents via cout
    operator const char*() const{
        return displayAs.c_str();
    }
};

int main () {
    set<ContactItem> setContacts;
    setContacts.insert(ContactItem("张三", "123456"));
    setContacts.insert(ContactItem("李四", "654321"));
    setContacts.insert(ContactItem("王五", "112233"));
    DisplayContents(setContacts);

    cout << "Enter a name you wish to delete: ";
    string nameToDelete;
    getline(cin, nameToDelete);
    
    auto contactFound = setContacts.find(ContactItem(nameToDelete, ""));
    if (contactFound != setContacts.end()) {
        setContacts.erase(contactFound);
        cout << "Contact deleted." << endl;
    } else {
        cout << "Contact not found." << endl;
    }

    DisplayContents(setContacts);
    return 0;
}

unordered_set和unordered_multiset

底层实现

  • set: 底层基于红黑树 (Red-Black Tree) 实现。红黑树是一种自平衡二叉查找树,它能保证插入、删除和查找操作的时间复杂度为 O(logN)。

  • unordered_set: 底层基于哈希表 (Hash Table) 实现。它使用哈希函数将元素映射到哈希表的桶 (bucket) 中,通过哈希冲突处理机制来解决多个元素映射到同一个桶的情况。

元素存储顺序

  • set: 元素是有序存储的。它会根据元素的键值进行排序(默认是升序)。当你遍历 set 中的元素时,你会得到一个有序的序列。

  • unordered_set: 元素是无序存储的。元素的存储位置由其哈希值决定,因此遍历 unordered_set 得到的元素顺序是不确定的。

性能

  • set:
    • 平均时间复杂度: 插入、删除、查找操作的时间复杂度都是 O(logN)。
    • 最坏时间复杂度: 也是 O(logN),因为红黑树是自平衡的。
  • unordered_set:
    • 平均时间复杂度: 插入、删除、查找操作的时间复杂度都是 O(1)。
    • 最坏时间复杂度: 在哈希冲突严重的情况下,所有元素都映射到同一个桶,哈希表会退化成链表,此时操作的时间复杂度会降为 O(N)。不过这种情况非常罕见,通常可以通过选择好的哈希函数和管理哈希表大小来避免。

使用 std::unordered_set 及其方法 insert( )、find( )、size( )、max_bucket_count( )、load_factor( )和 max_load_factor( )

#include <iostream>
#include <unordered_set>
using namespace std;

int main() {
    unordered_set<int> mySet;

    // 插入元素
    mySet.insert(10);
    mySet.insert(20);
    mySet.insert(30);

    // 查找元素
    auto it = mySet.find(20);
    if (it != mySet.end()) {
        cout << "Element found: " << *it << endl;
    } else {
        cout << "Element not found." << endl;
    }

    // 元素数量
    cout << "Size: " << mySet.size() << endl;

    // 最大桶数量
    cout << "Max bucket count: " << mySet.max_bucket_count() << endl;

    // 负载因子
    cout << "Load factor: " << mySet.load_factor() << endl;

    // 最大负载因子
    cout << "Max load factor: " << mySet.max_load_factor() << endl;

    return 0;
}

程序方法解释:


unordered_setunordered_map 的哈希表性能在很大程度上取决于其内部管理机制,max_bucket_countload_factormax_load_factor 就是其中几个关键参数。

1. max_bucket_count

  • 作用:表示容器所能容纳的最大桶(bucket)数量
  • 解释:这是一个只读成员函数,用于获取哈希表当前能支持的最大桶数。这个值通常由 C++ 标准库实现决定,并不会轻易改变。它代表了容器容量的一个上限,保证哈希表不会无限膨胀。当容器需要重新哈希(rehash)时,它会选择一个新的桶数,但这个新桶数不会超过 max_bucket_count

2. load_factor

  • 作用:衡量哈希表的拥挤程度
  • 计算方式load_factor = 元素数量(size) / 桶数量(bucket_count)
  • 解释load_factor 是一个动态变化的指标。它告诉你平均每个桶里有多少个元素。
    • load_factor:哈希表很稀疏,冲突少,查询速度快,但会占用更多内存。
    • load_factor:哈希表很拥挤,冲突多,查询速度变慢,但内存利用率高。

3. max_load_factor

  • 作用控制哈希表自动重新哈希的阈值
  • 解释:这是一个可读写的参数,可以由你手动设置。当哈希表的 load_factor 超过 max_load_factor 时,容器会自动进行重新哈希(rehash)操作。
  • 重新哈希:这是一种代价较高的操作。容器会创建一个新的、更大的哈希表(通常桶数量是之前的两倍),然后把旧哈希表中的所有元素重新插入到新哈希表中。这个过程会消耗较多时间和计算资源。

三者关系及实际应用

这三个参数共同协作来平衡 unordered_set 的性能和内存使用:

  1. 你通过 max_load_factor 设定一个“拥挤”的上限。
  2. 容器在插入新元素时,会实时计算 load_factor
  3. 一旦 load_factor 超过你设定的 max_load_factor,容器就会触发重新哈希。
  4. 重新哈希时,容器会增加桶的数量 (bucket_count),从而降低 load_factor,使其重新回到 max_load_factor 以下。新的桶数不会超过 max_bucket_count

对于 Android 开发者来说,如果你在 C++ 项目中使用了 unordered_setunordered_map,并遇到了性能瓶颈,通常可以通过手动调整 max_load_factor 来优化。如果你的数据量是已知的,或者对性能要求极高,你可以在容器初始化时使用 reserve() 函数预留足够的空间,避免频繁的重新哈希,这比动态调整 max_load_factor 更高效。

疑问:load_factor太大表示每个桶内元素过于拥挤时,重新hash为什么可以解决这个问题?

当哈希表的 max_load_factor(最大负载因子)被超过时,确实需要进行重新哈希(Rehashing),而不仅仅是简单地移动元素。这是因为哈希函数通常依赖于哈希表的容量。一个简单的哈希函数可能是 hash(key) % capacity 。当 capacity 改变时,同一个 key 得到的哈希值也就不一样了,即当容量增大后,即使是哈希值相同的两个键,在新的哈希表中也可能被分配到不同的位置,从而显著降低冲突的概率。

  • 请牢记,STL set 和 multiset 容器针对频繁查找 的情形进行了优化。
  • 请牢记,std::multiset 可存储多个值相同的元素 (键),而 std::set 只能存储不同的值。
  • 务必使用 multiset::count(value)确定有多少个元 素包含特定的值。
  • 请牢记,set::size( )和 multiset::size( )指出容器包 含多少个元素。
  • 对于其对象将存储在 set 或 multiset 等容器中的类,别忘了在其中实现运算符<和==。前者将成为排序谓词,而后者将用于 set::find( )等函数。
  • 在需要频繁插入而很少查找的情形下,不要使用 std::set 或 std::multiset;在这种情形下,std::vector和 std::list 通常更适合。

STL map和multimap

map 和 multimap 之间的区别在于,后者能够存储重复的键,而前者只能存储唯一的键。

为了实现快速查找,STL map 和 multimap 的内部结构看起来像棵二叉树。这意味着在 map 或 multimap 中插入元素时将进行排序。

map的实例化

典型的 map 实例化语法如下:

#include <map>
using namespace std;
...
map <keyType, valueType, Predicate=std::less <keyType>> mapObj;
multimap <keyType, valueType, Predicate=std::less <keyType>> mmapObj;

第三个模板参数是可选的。如果您值指定了键和值的类型,而省略了第三个模板参数,std::mapstd::multimap 将把 std::less<> 用作排序标准。

#include <map>
#include <iostream>
using namespace std;

int main() {
    map<int, string> myMap;
    myMap[1] = "one";
    myMap[2] = "two";
    myMap[3] = "three";

    for (auto it = myMap.begin(); it != myMap.end(); ++it) {
        cout << it->first << " => " << it->second << endl;
    }
    return 0;
}

map中插入元素

可以使用 insert() 方法插入元素,例如:

#include <map>
#include <iostream>
using namespace std;

int main() {
    map<int, string> myMap;
    myMap.insert(pair<int, string>(1, "one"));
    myMap.insert(pair<int, string>(2, "two"));
    myMap.insert(pair<int, string>(3, "three"));

    for (auto it = myMap.begin(); it != myMap.end(); ++it) {
        cout << it->first << " => " << it->second << endl;
    }
    return 0;
}

也可以采用数组方式添加元素,例如:

#include <map>
#include <iostream>
using namespace std;

int main() {
    map<int, string> myMap;
    myMap[1] = "one";
    myMap[2] = "two";
    myMap[3] = "three";

    for (auto it = myMap.begin(); it != myMap.end(); ++it) {
        cout << it->first << " => " << it->second << endl;
    }
    return 0;
}

类似于multiset,multimap中也可以使用 multimap::count() 指出有多少个元素包含指定的键。

map使用find()查找元素

find()函数总是返回一个迭代器。首先检查该迭代器,确保 find( )已成功,再使用它来访问找到的值。

#include <map>
#include <iostream>
using namespace std;

int main() {
    map<int, string> myMap;
    myMap[1] = "one";
    myMap[2] = "two";
    myMap[3] = "three";

    map<int, string>::iterator it = myMap.find(2);
    if (it != myMap.end()) {
        cout << "Found: " << it->first << " => " << it->second << endl;
    } else {
        cout << "Not found" << endl;
    }
    return 0;
}

如果使用的是 multimap,容器可能包含多个键相同的键-值对,因此需要找到与指定键对应的所有值。为此,可使用 multimap::count( ) 确定有多少个值与指定的键对应,再对迭代器递增,以访问这些相邻的值:

#include <map>
#include <iostream>
using namespace std;

int main() {
    multimap<int, string> myMap;
    myMap.insert(pair<int, string>(1, "one"));
    myMap.insert(pair<int, string>(2, "two"));
    myMap.insert(pair<int, string>(2, "two2"));
    myMap.insert(pair<int, string>(3, "three"));

    // 查找键为2的元素
    multimap<int, string>::iterator it = myMap.find(2);
    if (it != myMap.end()) {
        cout << "Found: " << it->first << " => " << it->second << endl;
    } else {
        cout << "Not found" << endl;
    }
    // 查找键为2的元素的数量
    int count = myMap.count(2);
    cout << "Count: " << count << endl;
    for (int i = 0; i < count; i++) {
        cout << "Found Again: " << it->first << " => " << it->second << endl;
        it++;
    }
    return 0;
}

使用erase()删除元素

类似于set,multimap也可以使用erase()删除元素。并且都是有三种类似的重载函数。

直接接收元素,接收迭代器,接收两个迭代器中间的范围:

#include <map>
#include <iostream>
using namespace std;

int main() {
    multimap<int, string> myMap;
    myMap.insert(pair<int, string>(1, "one"));
    myMap.insert(pair<int, string>(2, "two"));
    myMap.insert(pair<int, string>(2, "two2"));
    myMap.insert(pair<int, string>(3, "three"));

    // 删除键为2的元素
    myMap.erase(2);

    for (auto it = myMap.begin(); it != myMap.end(); ++it) {
        cout << it->first << " => " << it->second << endl;
    }

    auto it3 = myMap.find(3);
    // 删除迭代器指向的元素
    myMap.erase(it3);

    cout << "======>erase 3<========" << endl;

    for (auto it = myMap.begin(); it != myMap.end(); ++it) {
        cout << it->first << " => " << it->second << endl;
    }

    // 删除迭代器范围指向的元素
    myMap.erase(myMap.begin(), myMap.end());

    cout << "======>erase all<========" << endl;

    for (auto it = myMap.begin(); it != myMap.end(); ++it) {
        cout << it->first << " => " << it->second << endl;
    }

    return 0;
}

map编写排序谓词

要提供不同的排序标准,可编写一个二元谓词—实现了 operator() 的类或结构:

template<typename keyType>
struct Predicate
{
 bool operator()(const keyType& key1, const keyType& key2)
 {
 // your sort priority logic here
 }
};

例如一个电话薄场景,要求姓名 string 来对比的时候,不区分大小写,那么就可以编写一个谓词来实现:

#include <iostream>
#include <map>
#include <string>
#include<algorithm> 
using namespace std;

struct PredIgnoreCase
{
    bool operator()(const string& s1, const string& s2) const
    {
        string s1Lower = s1;
        string s2Lower = s2;
        transform(s1Lower.begin(), s1Lower.end(), s1Lower.begin(), ::tolower);
        transform(s2Lower.begin(), s2Lower.end(), s2Lower.begin(), ::tolower);
        return s1Lower < s2Lower;
    }
};

int main() {
    map<string, string> contactMap;
    contactMap["John"] = "123456";
    contactMap["jane"] = "789012";
    contactMap["alice"] = "345678";
    contactMap["Amy"] = "124525";

    for (auto it = contactMap.begin(); it != contactMap.end(); ++it) {
        cout << it->first << " => " << it->second << endl;
    }

    // 按照姓名排序
    cout << "======>sort by name<========" << endl;
    map<string, string, PredIgnoreCase> sortedMap(contactMap.begin(), contactMap.end());
    for (auto it = sortedMap.begin(); it != sortedMap.end(); ++it) {
        cout << it->first << " => " << it->second << endl;
    }
    return 0;
}

unordered_map

类似于unordered_set,unordered_map也可以使用哈希函数来存储元素,并且也可以使用自定义的哈希函数。

鉴于 unordered_map 将键-值对存储在桶中,在元素数达到或接近桶数时,它将自动执行负载均衡:

cout << "Load factor: " << umapIntToStr.load_factor() << endl;
cout << "Max load factor = " << umapIntToStr.max_load_factor() << endl;
cout << "Max bucket count = " << umapIntToStr.max_bucket_count() << endl;

load_factor() 指出了 unordered_map 桶的填满程度。因插入元素导致 load_factor() 超过 max_load_factor() 时,unordered_map 将重新组织以增加桶数,并重建散列表。

#include <iostream>
#include <unordered_map>
using namespace std;

int main() {
    unordered_map<int, string> umapIntToStr;
    umapIntToStr.insert(pair<int, string>(1, "one"));
    umapIntToStr.insert(pair<int, string>(2, "two"));
    umapIntToStr.insert(pair<int, string>(3, "three"));
    umapIntToStr.insert(pair<int, string>(4, "four"));
    umapIntToStr.insert(pair<int, string>(5, "five"));
    umapIntToStr.insert(pair<int, string>(6, "six"));
    umapIntToStr.insert(pair<int, string>(7, "seven"));
    umapIntToStr.insert(pair<int, string>(8, "eight"));
    umapIntToStr.insert(pair<int, string>(9, "nine"));
    umapIntToStr.insert(pair<int, string>(10, "ten"));
    umapIntToStr.insert(pair<int, string>(11, "eleven"));
    umapIntToStr.insert(pair<int, string>(12, "twelve"));
    umapIntToStr.insert(pair<int, string>(13, "thirteen"));
    
    cout << "Load factor: " << umapIntToStr.load_factor() << endl;
    cout << "Bucket Count:" << umapIntToStr.bucket_count() << endl;
    cout << "Max load factor = " << umapIntToStr.max_load_factor() << endl;
    cout << "Max bucket count = " << umapIntToStr.max_bucket_count() << endl;

    cout << "add 14th item" << endl;
 
    umapIntToStr.insert(pair<int, string>(14, "fourteen"));
    umapIntToStr.insert(pair<int, string>(15, "fifteen"));
    cout << "Load factor: " << umapIntToStr.load_factor() << endl;
     cout << "Bucket Count:" << umapIntToStr.bucket_count() << endl;
    cout << "Max load factor = " << umapIntToStr.max_load_factor() << endl;
    cout << "Max bucket count = " << umapIntToStr.max_bucket_count() << endl;
    return 0;
}

根据打印调整元素数量,一开始分配了13个bucket,当插入14个元素时,触发了rehash,分配了29个bucket。

注意:

  • 无论使用的键是什么,都不要编写依赖于 unordered_map 中元素排列顺序的代码。在unordered_map 中,元素相对顺序取决于众多因素,其中包括键、插入顺序、桶数等。这些容器为提高查找性能进行了优化,遍历其中的元素时,不要依赖于元素的排列顺序。
  • 在不发生冲突的情况下,std::unordered_map 的插入和查找时间几乎是固定的,不受包含的元素数的影响。然而,这并不意味着它优于在各种情形下复杂度都为对数的std::map。在包含的元素不太多的情况下,固定时间可能长得多,导致std::unordered_map 的速度比std::map 慢。选择容器类型时,务必执行模拟实际情况的基准测试。

map小结

  • 需要存储键-值对且键是唯一的时,务必使用map。
  • 需要存储键-值对且键可能重复时(如电话簿),务必使用 multimap。
  • 请牢记,与其他 STL 容器一样,map 和 multimap 都有成员方法 size() ,它指出容器包含多少个键-值对。
  • 必须确保插入和查找时间固定时(通常是包含的元素非常多时),务必使用 unordered_map 或unordered_multimap。
  • 别忘了, multimap::count(key) 指出容器中有多少个元素的键为 key。
  • 别忘了检查 find() 的返回值—将其与容器的 end() 进行比较。

函数对象

函数对象(Function Object)是 C++ 中的一种特殊类型的对象,它重载了 operator(),使得该对象可以像函数一样被调用。函数对象通常用于 STL 算法和容器中,以提供自定义的行为。

从概念上来说,函数对象是用作函数的对象;但从实现上说,函数对象是实现了 operator()的类的对象。虽然函数和函数指针也可归为函数对象,但实现了 operator()的类的对象才能保存状态(即类的成员属性的值),才能用于标准模板库(STL)算法。

C++程序员常用于 STL 算法的函数对象可分为下列两种类型。

  • 一元函数:接受一个参数的函数,如 f(x) 。如果一元函数返回一个布尔值,则该函数称为谓词。
  • 二元函数:接受两个参数的函数,如 f(x, y) 。如果二元函数返回一个布尔值,则该函数称为二元谓词。

返回布尔类型的函数对象通常用于需要进行判断的算法,如前面介绍的 find()和 sort()。组合两个函数对象的函数对象称为自适应函数对象。

一元函数

只对一个参数进行操作的函数称为一元函数。一元函数的功能可能很简单,如在屏幕上显示元素,如下所示:

// A unary function
template <typename elementType>
void FuncDisplayElement (const elementType& element)
{
    cout << element << ' ';
}; 

该函数也可采用另一种表现形式,即其实现 包含在类或结构的operator() 中:

// Struct that can behave as a unary function
template <typename elementType>
struct DisplayElement
{
 void operator () (const elementType& element) const
 {
 cout << element << ' ';
 }
}; 

这种形式的函数对象可以存储状态(即类的成员变量),并且可以在 STL 算法中使用。使用举例:

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>

using namespace std;

template <typename T>
struct DisplayElement {
    void operator() (const T& element) const {
        cout << element << "" << endl;
    }
};

int main() {
    vector<int> vec = {1, 2, 3, 4, 5};
    for_each(vec.begin(), vec.end(), DisplayElement<int>());

    list<char> list = {'a', 'b', 'c', 'd', 'e'};
    for_each(list.begin(), list.end(), DisplayElement<char>());
    cout << "using lambda" << endl;
    for_each(vec.begin(), vec.end(), [](int element) {
        cout << element << "" << endl;
    });
    cout << "using lambda" << endl;
    for_each(list.begin(), list.end(), [](char element) {
        cout << element << "" << endl;
    });
    return 0;
}

我们使用定义的这个结构体函数对象来打印int和char类型,最后的两个打印操作,使用到了lambda表达式,可以不用提前定义函数对象。

插入:操作符函数

操作符函数是重载了特定操作符的函数对象。通过重载操作符,可以使得对象能够像内置类型一样使用操作符进行操作。

例如之前用到的,对自定义的 MyString 类进行拼接时,重写 + 操作符,就可以直接使用 + 来操作对象。

class MyString {
public:
    ...

    MyString(const char* str) : data(str) {}

    // 重载 + 操作符
    MyString operator+(const MyString& other) const {
        return MyString((data + other.data).c_str());
    }

    ...
}

还有 == 操作符,用于对两个对象进行比较时来判断。例如在list中进行remove操作时。

这里的就是 () 操作符,在函数声明时用来定义 函数调用运算符 operator() 的。

结构中定义的优势

如果能够使用结构的对象来存储信息,则使用在结构中实现的函数对象的优点将显现出来。

我们设计一个结构,可以计算自己的 operator() 函数被调用的次数。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

template <typename T>
struct DisplayAndCount 
{
    int count;

    DisplayAndCount() : count(0) {}

    void operator() (const T& element) {
        cout << element << "" << endl;
        count++;
    }
};

int main() {
    vector<int> vec = {1, 2, 3, 4, 5};
    DisplayAndCount<int> displayAndCount;
    displayAndCount = for_each(vec.begin(), vec.end(), DisplayAndCount<int>());
    cout << "count: " << displayAndCount.count << endl;
    return 0;
}

对于displayAndCount = for_each(vec.begin(), vec.end(), DisplayAndCount<int>());这行代码,我们可以分步来理解:

1. for_each 函数的作用

首先,std::for_each 是 C++ 标准库中的一个算法,它的作用是对一个范围内的每个元素应用一个操作。

它的函数签名大致是这样的: for_each(InputIterator first, InputIterator last, UnaryFunction f)

  • firstlast:表示要遍历的范围,例如 vec.begin()vec.end()
  • f:一个可调用的对象(函数、函数指针、Lambda表达式或函数对象),它会对范围内的每个元素执行一次操作。

for_each 函数的一个关键特性是它的返回值:它返回它所传入的那个可调用对象的拷贝

2. DisplayAndCount<int>()

这部分代码创建了一个 DisplayAndCount<int> 类型的临时匿名对象。这个对象在作为第三个参数传递给 for_each 函数时,会发生以下过程:

  1. for_each 接收这个对象的一个拷贝
  2. for_each 遍历 vec 中的每个元素(1, 2, 3, 4, 5)。
  3. 对于每个元素,它都会调用这个拷贝对象的 operator() 方法。
  4. 在每次调用时,这个拷贝对象的 count 成员变量会递增。

3. for_each 的返回值

for_each 遍历完所有元素后,它会将 它内部那个 DisplayAndCount<int> 对象的拷贝 返回。此时,这个被返回的对象中的 count 已经变为了 5

4. 赋值操作

最后,这个 for_each 返回的、count 值为 5 的临时对象,通过赋值操作符 = 赋给了你在 main 函数中创建的 displayAndCount 对象。

思考:是否可以使用按引用传递的方式来传递这个函数对象,而不用接收匿名对象的拷贝?

回答:可以的,我们使用for_each算法时,将函数对象的参数使用 std::ref() 包裹起来,这样就可以避免拷贝构造函数的调用。但是为什么 STL 算法通过返回值来传递结果是一种常见模式,这在 C++ 标准库中非常普遍。

使用值传递拷贝并返回,而不是按引用。设计上的考量主要有三点:

  • 一是优化和内联,像这种轻量级的函数对象,可以很轻松的内联避免函数调用开销。并且在运行过程中,这个对象就成了栈上的局部变量,可以进行更激进的寄存器分配和优化。如果使用引用传递,编译器在优化时会变得更保守,因为它必须考虑到你传入的这个引用可能指向一个全局变量、堆内存或其他地方,这会使得它无法像处理局部变量那样简单地进行优化。
  • 二是线程安全考虑,如果 for_each 默认使用引用传递,那么在多线程环境中调用它会变得很危险。多个线程可能会同时使用同一个仿函数对象,导致数据竞争(data race),因为它们都在试图修改同一个 count 变量,从而产生未定义的行为。而值传递方案则能天然地解决这个问题。每个线程调用 for_each 时,都会得到一个独立的、属于它自己的仿函数副本。这个副本只会在该线程内部被修改,因此不同线程之间不会互相干扰,实现了线程安全。
  • 第三是设计模式,在函数式编程中,函数不应该有副作用(side effect),即不应该修改传入的参数或者任何外部状态。for_each 通过值传递仿函数,并返回一个新对象来承载结果,这种模式很好地遵循了这一原则。

一元谓词

一元谓词是接受一个参数并返回布尔值的函数对象。它通常用于 STL 算法中进行条件判断。

下面这个例子,设计一个IsMultiple类,用于判断一个数是否是另一个数的倍数。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

template <typename T>
struct IsMultiple {
    T multiple;
    IsMultiple(T multiple) : multiple(multiple) {}
    bool operator() (T element) const {
        return ((element % multiple) == 0);
    }
};

int main() {
    vector<int> vec = {1, 2, 3, 4, 5};
    
    int divisor = 2;

    auto it = find_if(vec.begin(), vec.end(), IsMultiple<int>(divisor));

    if (it != vec.end()) {
        cout << "找到第一个倍数: " << *it << endl;
    } else {
        cout << "没有找到倍数" << endl;
    }

    return 0;
}

find_if() 使用了一元谓词,这里将函数对象 IsMutilple 初始化为用户提供的除数, find_if() 对指定范围内的每个元素调用一元谓词 IsMutilple::operator() 。当 operator() 返回 true(即元素可被用户提供的除数整除)时,find_if() 返回一个指向该元素的迭代器。然后,将 find_if() 操作的结果与容器的 end() 进行比较,以核实是否找到了满足条件的元素,接下来使用迭代器 iElement 显示该元素的值。

一元谓词被大量用于 STL 算法中。例如,算法 std::partition() 使用一元谓词来划分范围,算法 stable_partition() 也使用一元谓词来划分范围,但保持元素的相对顺序不变。诸如 std::find_if() 等查找函数以及 std::remove_if() 等删除元素的函数也使用一元谓词,其中 std::remove_if() 删除指定范围内满足谓词条件的元素。

二元函数

二元函数是接受两个参数并返回一个值的函数对象。它通常用于 STL 算法中进行元素操作。

下面这个例子,设计一个 Multiply 结构体,来计算两个数的乘积。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

template <typename T>
struct Multiply 
{
    T operator() (const T& a, const T& b) const {
        return a * b;
    }
};

int main() {
    vector<int> vec = {0, 1, 2, 3, 4};
    vector<int> vec2 = {1, 2, 3, 4, 5};
    vector<int> vecResult;
    vecResult.resize(vec2.size());
    
    transform(vec.begin(), vec.end(), vec2.begin(), vecResult.begin(), Multiply<int>());
    cout << "vecResult: " << endl;
    for (int i : vecResult) {
        cout << i << endl;
    }

    return 0;
}

二元谓词

二元谓词是接受两个参数并返回布尔值的函数对象。它通常用于 STL 算法中进行比较操作。

举之前的一个例子,接受两个字符串,进行不区分大小写的比较。常用于电话簿,app列表等。

#include <iostream>
#include <set>
#include <string>
#include <algorithm>
using namespace std;

template <typename S>
struct CompareStringNoCase
{
    bool operator() (const S& a, const S& b) const
    {
        string aLower;
        string bLower;
        aLower.resize(a.size());
        bLower.resize(b.size());
        transform(a.begin(), a.end(), aLower.begin(), ::tolower);
        transform(b.begin(), b.end(), bLower.begin(), ::tolower);
        return aLower < bLower;
    }
};

int main()
{
    set<string, CompareStringNoCase<string>> setContacts;
    setContacts.insert("Amy");
    setContacts.insert("Bob");
    setContacts.insert("Caroline");
    setContacts.insert("alice");
    setContacts.insert("dave");
    setContacts.insert("Eve");
    setContacts.insert("carl");

    for (const string& contact : setContacts)
    {
        cout << contact << endl;
    }

    return 0;
}

lambda 表达式

可将 lambda 表达式视为包含公有 operator() 的匿名结构(或类),从这种意义上说,lambda 表达式属于前面介绍的函数对象。

例如将之前的 DisplayElement 类,用 lambda 表达式来实现。

// struct that behaves as a unary function
template <typename elementType>
struct DisplayElement
{
 void operator () (const elementType& element) const
 {
 cout << element << ' ';
 }
};
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;
int main() {
    vector<int> vec = {1, 2, 3, 4, 5};
    for_each(vec.begin(), vec.end(), [](int element) {
        cout << element << "" << endl;
    });
    return 0;
}

编译器见到上述lambda表达式,会将其转换为一个匿名的函数对象,该函数对象的 operator() 函数体就是lambda表达式的函数体。

struct NoName
{
 void operator () (const int& element) const
 {
 cout << element << ' ';
 }
}; 

一元函数的lambda表达式

与一元 operator(Type) 对应的 lambda 表达式接受一个参数,其定义如下:

[](Type paramName) { 
    // lambda expression code here;
}

请注意,如果您愿意,也可按引用传递参数:

[](Type& paramName) { 
    // lambda expression code here;
} 

另外,lambda表达式的参数声明中,也可以直接使用auto,编译器会根据lambda表达式的参数类型,自动推导参数类型。

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> vec = {1, 2, 3, 4, 5};
    for_each(vec.begin(), vec.end(), [](auto element) {
        cout << element << "" << endl;
    });

    list<char> charList = {'a', 'b', 'c', 'd', 'e'};
    for_each(charList.begin(), charList.end(), [](auto element) {
        cout << element << "" << endl;
    });
    return 0;
}

这个 lambda 表达式通过关键字 auto 利用了编译器的类型自动推断功能。遵循 C++14 的编译器都支持这种对 lambda 表达式的改进。

一元谓词的lambda表达式

例如判断一个整数是否是偶数,在返回结果里写成是否可以被2整除。编译器可以自动判断返回值为bool。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> vec = {1, 2, 3, 4, 5};
    auto it = find_if(vec.begin(), vec.end(), [](const int& element) {
       return element % 2 == 0;
    });
    if (it != vec.end()) {
        cout << "找到第一个偶数: " << *it << endl;
    } else {
        cout << "没有找到偶数" << endl;
    }
    return 0;
}

插入:符号[]的作用

C++ lambda 表达式中的 [] 叫做 capture clause(捕获子句),它的作用是让 lambda 表达式能够访问其创建时上下文(surrounding scope)中的变量。即用于指定在 lambda 表达式中可以访问的变量或对象。它主要有以下几种用法:

1. 不捕获任何变量 []

这是最基本的形式,表示 lambda 表达式不访问任何外部变量。

#include <iostream>

int main() {
    int x = 10;
    auto my_lambda = []() {
        // 无法访问 x,因为没有加入捕获
        std::cout << "Hello, World!" << std::endl;
    };
    my_lambda();
    return 0;
}

2. 按值捕获 [var][=]

  • 按值捕获单个变量: [x]
    • x 的值在 lambda 创建时被 复制 到 lambda 内部。
    • 即使外部的 x 后来改变了,lambda 内部的 x 也不会受影响。
#include <iostream>

int main() {
    int x = 10;
    auto my_lambda = [x]() {
        // x 的值是 10
        std::cout << "The value of x is: " << x << std::endl;
    };
    x = 20; // 改变外部的 x
    my_lambda(); // 输出依然是 10
    return 0;
}
  • 按值捕获所有变量: [=]
    • 捕获 其所在作用域中 所有被 lambda 内部使用的局部变量。
    • 同样是值复制,不会随外部变量变化。
#include <iostream>
int main() {
    int a = 1, b = 2;
    auto my_lambda = [=]() {
        // a 和 b 的值在 lambda 创建时被复制
        std::cout << "The value of a is: " << a << std::endl;
        std::cout << "The value of b is: " << b << std::endl;
    };
    a = 3; // 改变外部的 a
    my_lambda(); // 输出依然是 1 和 2
    return 0;
}

3. 按引用捕获 [&var][&]

  • 按引用捕获单个变量: [&x]
    • x 的引用被传递到 lambda 内部。
    • 这意味着 lambda 内部对 x 的任何修改都会影响到外部的 x
    • 当外部 x 的值改变时,lambda 内部也能看到最新的值。
#include <iostream>

int main() {
    int x = 10;
    auto my_lambda = [&x]() {
        // 访问的是外部的 x
        x += 5; // 修改了外部的 x
        std::cout << "The value of x inside lambda is: " << x << std::endl;
    };
    my_lambda(); // x 变为 15
    std::cout << "The value of x outside lambda is: " << x << std::endl; // 输出 15
    return 0;
}
  • 按引用捕获所有变量: [&]
    • 捕获其所在作用域中所有被 lambda 内部使用的局部变量的引用。
    • 这种方式非常方便,但要小心,因为它可能导致悬空引用(dangling reference)问题,特别是当 lambda 的生命周期比它捕获的变量更长时。

4. 混合捕获 [&, x][=, &x]

你可以混合使用值捕获和引用捕获:

  • [&, x] 默认按引用捕获所有变量,但 x 单独按值捕获。
  • [=, &x] 默认按值捕获所有变量,但 x 按引用捕获。
#include <iostream>

int main() {
    int a = 1, b = 2;
    // 默认按值捕获,但 b 按引用捕获
    auto my_lambda = [=, &b]() {
        // a 是值捕获,不能修改
        std::cout << "The value of a is: " << a << std::endl;
        // b 是引用捕获,可以修改
        b = 3;
    };
    my_lambda();
    std::cout << "The value of b outside lambda is: " << b << std::endl; // 输出 3
    return 0;
}

总而言之,[] 括号就是 C++ 中 lambda 表达式的 捕获列表 ,它决定了 lambda 如何与外部世界的变量进行交互。正确使用捕获子句是编写强大、灵活的 lambda 表达式的关键。

例如,除了找到列表中能被 2 整除的数,我们希望可以由用户指定除数,增加灵活性。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> numVec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int divisor = 0;
    cout << "请输入除数:" << endl;
    cin >> divisor;

    auto it = find_if(numVec.begin(), numVec.end(), [divisor](int num) {
        return num % divisor == 0;
    });

    if (it != numVec.end())
    {
        cout << "找到第一个能被" << divisor << "整除的数: " << *it << endl;
    }
    else
    {
        cout << "没有找到能被" << divisor << "整除的数" << endl;
    }
    return 0;
}

二元函数的lambda表达式

二元函数接受两个参数,可以返回一个值。

例如,将两个vector中的各个元素对应相加,并将结果存储到第三个vector中。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> vec1 = {1, 2, 3, 4, 5};
    vector<int> vec2 = {0, 1, 2, 3, 4};
    
    vector<int> vecResult;
    vecResult.resize(vec2.size());
    
    transform(vec1.begin(), vec1.end(), vec2.begin(), vecResult.begin(), [](const int& a, const int& b) {
        return a+b;
    });
    
    for(auto it = vecResult.begin();it!=vecResult.end(); it++){
        cout << *it <<" ";
    }
    cout << endl;
    
    return 0;
}

定义Lambda表达式时,习惯性在末尾加上了const。导致报错,原因如下:Lambda 表达式末尾的 const 是用来修饰 lambda 本身的,表示这个 lambda 是一个“常量”函数对象。它主要用于确保 lambda 内部不会改变其按值捕获的变量。当你没有按值捕获变量时,这个 const 并没有太大意义。 std::transform 等标准库算法所期望的函数对象通常不带这个 const 修饰符,所以加上它会导致签名不匹配的编译错误。

二元谓词对应的 lambda 表达式

返回 true 或 false、可帮助决策的二元函数被称为二元谓词。这种谓词可用于 std::sort() 等排序算法中,这些算法对容器中的两个值调用二元谓词,以确定将哪个放在前面。与二元谓词等价的 lambda 表达式的通用语法如下:

[...](Type1& param1Name, Type2& param2Name) { 
    // return bool expression; 
} 

例如,对一个姓名数组中的元素进行排序,忽略大小写按照字母表顺序:

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

using namespace std;


int main()
{
    vector<string> nameVec = {"Stephenie", "Amy", "Bob", "Cindy", "David", "Eva"};

    sort(nameVec.begin(), nameVec.end(), [](const string& a, const string& b) {
        string aLower = a;
        string bLower = b;
        transform(aLower.begin(), aLower.end(), aLower.begin(), ::tolower);
        transform(bLower.begin(), bLower.end(), bLower.begin(), ::tolower);
        return aLower < bLower;
    });

    for(auto it = nameVec.begin();it!=nameVec.end(); it++){
        cout << *it <<"\t";
    }
    cout << endl;

    return 0;
}

lambda小结

  • 请牢记,lambda 表达式总是以[]或 [state1,state2,…] 打头。
  • 请牢记,除非使用关键字 mutable 进行指定,否则不能修改捕获列表中指定的状态变量。
  • 别忘了,lambda 表达式是实现了 operator( )的匿名类(或结构)。
  • 编写 lambda 表达式时,别忘了使用 const 对参数进行限定:

      [](const T& value) {
          // lambdaexpression; 
      }
    
  • lambda 表达式的语句块({})包含多条语句时,别忘了显式地指定返回类型。
  • 不要使用很长(包含多条语句)的 lambda 表达式,而应转而使用函数对象,因为每次使用 lambda 表达式时,都需要重新定义它,这无助于提高代码 的可重用性。

STL算法

查找、搜索、删除和计数是一些通用算法,其应用范围很广。STL 通过通用的模板函数提供了这些算法以及其他的很多算法,可通过迭代器对容器进行操作。要使用 STL 算法,程序员必须包含头文件<algorithm>

STL 算法通常分为两大类:非变序算法(Non-modifying Algorithms)变序算法(Modifying Algorithms)

非变序算法 (Non-modifying Algorithms)

非变序算法不会改变它们操作的元素的值或顺序。它们主要用于查询、计数、查找和比较操作。

常见示例:

  • std::for_each(): 对范围内的每个元素执行一个指定的函数。虽然它本身不改变容器的结构,但你传递给它的函数可以修改元素的值。
  • std::find(): 在一个范围内查找第一个匹配给定值的元素。
  • std::find_if(): 在一个范围内查找第一个满足特定条件的元素。
  • find_end(): 在指定范围内搜索最后一个满足特定条件的元素。
  • std::count(): 计算一个范围内某个特定值的出现次数。
  • std::count_if(): 计算一个范围内满足特定条件的元素的数量。
  • std::equal(): 检查两个范围内的元素是否完全相等。
  • std::all_of(): 检查一个范围内所有元素是否都满足某个条件。
  • std::any_of(): 检查一个范围内是否有任何元素满足某个条件。
  • std::none_of(): 检查一个范围内是否没有任何元素满足某个条件。
  • std::mismatch(): 比较两个范围,返回第一个不匹配的元素对。
  • search_n() 在目标范围内搜索与指定值相等或满足指定谓词的 n 个元素
  • find_first_of(): 在目标范围内搜索指定序列中的任何一个元素第一次出现的位置;在另一个重载版本中,它搜索满足指定条件的第一个元素
  • adjacent_find(): 在集合中搜索两个相等或满足指定条件的元素

比较算法

  • equal(): 比较两个元素是否相等或使用指定的二元谓词判断两者是否相等
  • mismatch(): 使用指定的二元谓词找出两个元素范围的第一个不同的地方
  • lexicographical_compare(): 比较两个序列中的元素,以判断哪个序列更小

变序算法 (Modifying Algorithms)

变序算法会改变它们操作的元素的值或顺序。这包括排序、复制、替换、删除和排列等操作。

常见示例:

初始化算法

  • fill(): 将指定值分配给指定范围中的每个元素
  • fill_n(): 将指定值分配给指定范围中的前 n 个元素
  • generate(): 将指定函数对象的返回值分配给指定范围中的每个元素
  • generate_n(): 将指定函数的返回值分配给指定范围中的前 n 个元素 修改算法
  • for_each(): 对指定范围内的每个元素执行指定的操作。当指定的参数修改了范围时,for_each 将是变序算法
  • transform(): 对指定范围中的每个元素执行指定的一元函数复制算法
  • copy(): 将一个范围复制到另一个范围
  • copy_backward(): 将一个范围复制到另一个范围,但在目标范围中将元素的排列顺序反转

删除算法

  • remove(): 将指定范围中包含指定值的元素删除
  • remove_if(): 将指定范围中满足指定一元谓词的元素删除
  • remove_copy(): 将源范围中除包含指定值外的所有元素复制到目标范围
  • remove_copy_if(): 将源范围中除满足指定一元谓词外的所有元素复制到目标范围
  • unique(): 比较指定范围内的相邻元素,并删除重复的元素。该算法还有一个重载版本,它使用二元谓词来判断要删除哪些元素
  • unique_copy(): 将源范围内的所有元素复制到目标范围,但相邻的重复元素除外

替换算法

  • replace(): 用一个值来替换指定范围中与指定值匹配的所有元素
  • replace_if(): 用一个值来替换指定范围中满足指定条件的所有元素

排序算法

  • sort(): 使用指定的排序标准对指定范围内的元素进行排序,排序标准由二元谓词提供。排序可能改变相等元素的相对顺序
  • stable_sort(): 类似于 sort,但在排序时保持相对顺序不变
  • partial_sort(): 将源范围内指定数量的元素排序
  • partial_sort_copy(): 将源范围内的元素复制到目标范围,同时对它们排序

分区算法

  • partition(): 在指定范围中,将元素分为两组:满足指定一元谓词的元素放在第一个组中,其他元素放在第二组中。不一定会保持集合中元素的相对顺序
  • stable_partition(): 与 partition 一样将指定范围分为两组,但保持元素的相对顺序不变 可用于有序容器的算法
  • binary_search(): 用于判断一个元素是否存在于一个排序集合中
  • equal_range(): 返回一个范围,该范围包含所有与指定值相等的元素
  • lower_bound(): 根据元素的值或二元谓词判断元素可能插入到排序集合中的第一个位置,并返回一个指向该位置的迭代器
  • upper_bound(): 根据元素的值或二元谓词判断元素可能插入到排序集合中的最后一个位置,并返回一个指向该位置的迭代器

这些只是 STL 算法库中的一部分,但它们是最常用和最基础的。掌握它们可以让你用更简洁、更高效的方式编写 C++ 代码。

使用:查找元素

STL 算法 find()find_if() 用于在 vector 等容器中查找与值匹配或满足条件的元素。find() 的用法如下:

auto element = find (numsInVec.cbegin(), // Start of range
 numsInVec.cend(), // End of range
 numToFind); // Element to find
// Check if find() succeeded
if (element != numsInVec.cend ())
 cout << "Result: Value found!" << endl;

find_if() 的用法与此类似,但需要通过第三个参数提供一个一元谓词(返回 true 或 false 的一元函数):

auto evenNum = find_if (numsInVec.cbegin(), // Start of range
    numsInVec.cend(), // End of range
    [](int element) { return (element % 2) == 0; } );
    
if (evenNum != numsInVec.cend())
 cout << "Result: Value found!" << endl; 

计算符合条件的元素数量

STL 算法 std::count()count_if() 计算给定范围内的元素数。std::count() 计算包含给定值(使用相等运算符 == 进行测试)的元素数:

size_t numZeroes = count (numsInVec.cbegin (), numsInVec.cend (), 0);
cout << "Number of instances of '0': " << numZeroes << endl;

std::count_if() 计算这样的元素数,即满足通过参数传递的一元谓词(可以是函数对象,也可以是lambda 表达式):

// Unary predicate:
template <typename elementType>
bool IsEven (const elementType& number)
{
 return ((number % 2) == 0); // true, if even
}
...
// Use the count_if algorithm with the unary predicate IsEven:
size_t numEvenNums = count_if (numsInVec.cbegin (),
 numsInVec.cend (), IsEven <int> );
cout << "Number of even elements: " << numEvenNums << endl; 

使用举例:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

template <typename elementType>
bool IsEven (const elementType& number)
{
 return ((number % 2) == 0); // true, if even
}

int main() {
    vector<int> numsInVec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    size_t numZeroes = count (numsInVec.cbegin (), numsInVec.cend (), 0);
    cout << "Number of instances of '0': " << numZeroes << endl;

    size_t numEvenNums = count_if (numsInVec.cbegin (),
     numsInVec.cend (), IsEven <int> );
    cout << "Number of even elements: " << numEvenNums << endl; 

    return 0;
}

搜索元素或者序列

STL 算法 search() 用于在一个范围内搜索另一个范围的序列。它有两个重载版本:

template <typename ForwardIterator1, typename ForwardIterator2>
ForwardIterator1 search (ForwardIterator1 first1, ForwardIterator1 last1,
    ForwardIterator2 first2, ForwardIterator2 last2);
template <typename ForwardIterator1, typename ForwardIterator2, typename BinaryPredicate>

ForwardIterator1 search (ForwardIterator1 first1, ForwardIterator1 last1,
    ForwardIterator2 first2, ForwardIterator2 last2,
    BinaryPredicate pred);

第一个版本使用相等运算符==来比较元素,第二个版本使用传递的二元谓词来比较元素。

auto range = search (numsInVec.cbegin(), // Start range to search in
 numsInVec.cend(), // End range to search in
 numsInList.cbegin(), // start range to search
 numsInList.cend() ); // End range to search for 

算法 search_n() 用于在一个范围内搜索指定数量的连续元素,该元素与指定值匹配或满足指定条件。

auto partialRange = search_n (numsInVec.cbegin(), // Start range
                            numsInVec.cend(), // End range
                             3, // num items to be searched for
                             9  // value to search for
);

这两个函数都返回一个迭代器,它指向找到的第一个模式;使用该迭代器之前,务必将其与 end() 进行比较。

用法演示:

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> numsInVec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    list<int> numsInList = {9, 10, 11};
    list<int> numsInList2 = {9, 10, 11, 12};

    auto range = search (numsInVec.cbegin(), // Start range to search in
                        numsInVec.cend(), // End range to search in
                        numsInList.cbegin(), // start range to search
                        numsInList.cend() ); // End range to search for 

    if (range != numsInVec.cend())
    {
        cout << "Range found at position: " << distance(numsInVec.cbegin(), range) << endl;
    }
    else
    {
        cout << "Range not found" << endl;
    }

    range = search (numsInVec.cbegin(), // Start range to search in
                        numsInVec.cend(), // End range to search in
                        numsInList2.cbegin(), // start range to search
                        numsInList2.cend() ); // End range to search for 
                        
    if (range != numsInVec.cend())
    {
        cout << "Range found at position: " << distance(numsInVec.cbegin(), range) << endl;
    }
    else
    {
        cout << "Range not found" << endl;
    }

    return 0;
}

search_n() 使用举例:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> numsInVec = {1, 2, 3, 9, 9, 9, 9, 10};

    auto range = search_n(numsInVec.cbegin(), // Start range to search in
                          numsInVec.cend(),   // End range to search in
                          3, 9);              // End range to search for

    if (range != numsInVec.cend())
    {
        cout << "999 found at position: " << distance(numsInVec.cbegin(), range) << endl;
    }
    else
    {
        cout << "Range not found" << endl;
    }

    return 0;
}

使用fill

STL 算法 fill()fill_n() 用于将指定范围的内容设置为指定值。fill() 将指定范围内的元素设置为指定值:

vector <int> numsInVec (3);
// fill all elements in the container with value 9
fill (numsInVec.begin (), numsInVec.end (), 9);

顾名思义,fill_n() 将 n 个元素设置为指定的值,接受的参数包括起始位置、元素数以及要设置 的值:

fill_n (numsInVec.begin () + 3, /*count*/ 3, /*fill value*/ -9); 

使用示例:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

template <typename T>
void DisplayContents(const T& container)
{
    for(auto it = container.begin(); it != container.end(); ++it)
        cout << *it << " ";
    cout << endl;
}

int main()
{
    vector<int> numsInVec(3);

    fill(numsInVec.begin(), numsInVec.end(), 9);

    DisplayContents(numsInVec);
    // Output: 9 9 9

    numsInVec.resize(6);

    fill_n(numsInVec.begin() + 3, 3, 7);
    // Output: 9 9 9 7 7 7

    DisplayContents(numsInVec);

    return 0;
}

fill函数要修改容器的元素值,就不可以使用cbegin()常量迭代器,cbegin()不能用于修改容器的元素值。

使用 std::generate() 将元素设置为运行阶段生成的值

std::generate() 是 C++ 标准库 <algorithm> 中的一个泛型算法,它的主要作用是根据一个生成器函数(generator function)来为指定范围内的所有元素赋值。

简单来说,它的功能就是:用一个函数不断地生成值,并把这些值依次填充到一个容器或数组中。

std::generate() 的工作流程非常直接:

  1. 它从 first 迭代器指向的元素开始。
  2. 对范围中的每一个元素,它都会调用一次你提供的 Generator g
  3. g 的返回值会被赋给当前元素。
  4. 它将迭代器向前移动,然后重复这个过程,直到到达 last 迭代器为止。

1. 使用 Lambda 表达式生成递增序列

这个例子使用一个 Lambda 表达式作为生成器,每次调用时让一个变量加2。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> numsInVec(5);

    int start_value = 2;

    generate(numsInVec.begin(), numsInVec.end(), [&]()
             { return start_value += 2; });

    for_each(numsInVec.begin(), numsInVec.end(), [](int &n)
             { cout << n << endl; });

    return 0;
}
  • 这里的 Lambda 表达式捕获了外部变量 start_value 的引用,每次被调用时都会返回 start_value 的当前值,然后将其递增。

注意细节,符号运算 +=++ 不可混淆,后置自增(i++)是先创建一个副本,再增加原始变量,最后返回副本,所以看起来像是先返回再计算。而这个 += 符号就是直接运算,然后返回计算后的结果。

2. 使用函数生成随机数

这个例子使用一个自定义函数作为生成器来填充随机数。

#include <iostream>
#include <vector>
#include <algorithm>
#include <random> // C++11 之后的标准库随机数生成

// 生成随机数的函数
int generate_random_number() {
    static std::mt19937 generator(std::random_device{}()); // 静态生成器,只初始化一次
    static std::uniform_int_distribution<int> distribution(1, 100); // 1到100的均匀分布
    return distribution(generator);
}

int main() {
    std::vector<int> random_numbers(5);

    // 将函数名作为生成器传递给 generate()
    std::generate(random_numbers.begin(), random_numbers.end(), generate_random_number);

    for (int num : random_numbers) {
        std::cout << num << " "; // 输出类似: 56 12 87 34 99
    }
    std::cout << std::endl;

    return 0;
}
  • generate() 的第三个参数可以接受任何可调用对象,这里我们直接传入了函数名 generate_random_number

总结

std::generate() 是一个非常有用的工具,当你需要用重复的、有规律的或随机的方法来填充一个序列时,它比手写一个 for 循环要更简洁、更具表达力。

对于生成递增或递减的序列,C++ 标准库还提供了更专用的 std::iota() 函数,它在某些场景下会更清晰。但 generate() 的优势在于其通用性,你可以用它来处理任何可以通过一个无参数函数来生成值的场景。

使用 for_each() 处理指定范围内的元素

算法 for_each() 对指定范围内的每个元素执行指定的一元函数对象,其用法如下:

fnObjType retValue = for_each (start_of_range,
 end_of_range,
 unaryFunctionObject);

也可使用接受一个参数的 lambda 表达式代替一元函数对象。

返回值表明,for_each() 返回用于对指定范围内的每个元素进行处理的函数对象(functor)。这意味着使用结构或类作为函数对象可存储状态信息,并在 for_each() 执行完毕后查询这些信息。

实例,打印列表内每一个元素,并计算打印了多少次:

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

template <typename T>
struct DisplayAndCount
{
    int count = 0;
    DisplayAndCount() : count(0) {}
    void operator()(const T &value)
    {
        cout << value << " ";
        count++;
    }
};

int main()
{
    vector<int> numbers = {1, 2, 3, 4, 5};
    DisplayAndCount<int> displayAndCount;
    displayAndCount = for_each(numbers.cbegin(), numbers.cend(), DisplayAndCount<int>());
    cout << endl;
    cout << "Count: " << displayAndCount.count << endl;

    string text = "we are the world.";
    for_each(text.cbegin(), text.cend(), [](const char &c)
             { cout << c << " "; });
    cout << endl;

    return 0;
}

后面演示了使用lambda表达式来遍历操作每个元素的方式。

使用 std::transform() 对范围进行变化

std::for_each()std::transform() 很像,都对源范围内的每个元素调用指定的函数对象。然而, std::transform() 有两个版本,第一个版本一个接受一元函数,常用于将字符串转换为大写或小写(使用的一元函数分别是 toupper()tolower() ):

string str ("THIS is a TEst string!");
transform (str.cbegin(), // start source range
 str.cend(), // end source range
 strLowerCaseCopy.begin(), // start destination range
 ::tolower); // unary function

第二个版本接受一个二元函数,让 transform() 能够处理一对 来自两个不同范围的元素

// sum elements from two vectors and store result in a deque
transform (numsInVec1.cbegin(), // start of source range 1
 numsInVec1.cend(), // end of source range 1
 numsInVec2.cbegin(), // start of source range 2
 sumInDeque.begin(), // store result in a deque
 plus<int>()); // binary function plus

不像 for_each() 那样只处理一个范围,这两个版本的 transform() 都将指定变换函数的结果赋给指定的目标范围。

使用 std::transform() ,对两个列表的元素进行相加的实例。之前已经写过这个例子,有意思的是, 如果两个列表的元素数量不一致, transform() 函数的表现是怎样的呢?

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> numbersOne = {1, 2, 3, 4, 5};
    vector<int> numbersTwo = {6, 7, 8, 9, 10, 20};

    vector<int> result(10);

    transform(numbersOne.begin(), numbersOne.end(), numbersTwo.begin(), result.begin(), [](int a, int b)
              { return a + b; });

    for (int num : result)
    {
        cout << num << " ";
    }

    return 0;
}

这里的 transform() 函数的行为是,将 numbersOnenumbersTwo 中对应位置的元素相加,结果存储在 result 中。如果 numbersOnenumbersTwo 的元素数量不一致, transform() 函数会以第一个元素为基准,遍历完第一个列表,第二个列表缺失的话,第二个元素会被置为默认值,例如int元素处理为0,string元素处理为空字符串。

如果承载的容器大小不足,例如result长度设置为 3 会发生什么呢?经运行测试,在 result 装满之后没有自动扩容,也没有报错。

复制元素和删除元素

  • copy() 复制元素,copy 沿向前的方向将源范围的内容赋给目标范围:

      auto lastElement = copy (numsInList.cbegin(), // start source range
      numsInList.cend(), // end source range
      numsInVec.begin()); // start dest range
    
  • copy_if() 是 C++11 新增的,仅在指定的一元谓词返回 true 时才复制元素:

      // copy odd numbers from list into vector
      copy_if (numsInList.cbegin(), numsInList.cend(),
      lastElement, // copy position in dest range
      [](int element){return ((element % 2) == 1);});
    
  • copy_backward() 沿向后的方向将源范围的内容赋给目标范围,这并不是从后往前复制一个翻转的列表,而是将源范围的元素从后往前复制到目标范围的指定位置。主要用于处理 重叠内存区域 ,特别是在你想要将一个序列向后移动(比如在 vector 中间插入一个元素时,需要将后面的元素向后移)。它通过从后往前复制,确保 在将数据复制到新位置之前,旧位置的数据不会被覆盖

      copy_backward (numsInList.cbegin (),
      numsInList.cend (),
      numsInVec.end ());
    
  • remove( )将容器中与指定值匹配的元素删除:

      // Remove all instances of '0', resize vector using erase()
      auto newEnd = remove (numsInVec.begin (), numsInVec.end (), 0);
      numsInVec.erase (newEnd, numsInVec.end ());
    
  • remove_if( )使用一个一元谓词,并将容器中满足该谓词的元素删除:

      // Remove all odd numbers from the vector using remove_if
      newEnd = remove_if (numsInVec.begin (), numsInVec.end (),
      [](int num) {return ((num % 2) == 1);} ); //predicate
      numsInVec.erase (newEnd, numsInVec.end ()); // resizing 
    

使用实例:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{ 
    vector<int> numsInVec = {1, 2, 3, 4, 5, 0, 6, 7, 8, 9, 0};

    // Copy elements from numsInVec to another vector
    vector<int> copiedVec(numsInVec.size());
    copy(numsInVec.begin(), numsInVec.end(), copiedVec.begin());

    cout << "Copied Vector: ";
    for (const auto &num : copiedVec)
        cout << num << " ";
    cout << endl;

    // Remove all instances of '0'
    auto newEnd = remove(numsInVec.begin(), numsInVec.end(), 0);
    numsInVec.erase(newEnd, numsInVec.end());

    cout << "After removing '0': ";
    for (const auto &num : numsInVec)
        cout << num << " ";
    cout << endl;

    // Remove all odd numbers
    newEnd = remove_if(numsInVec.begin(), numsInVec.end(),
                       [](int num) { return (num % 2) == 1; });
    numsInVec.erase(newEnd, numsInVec.end());

    cout << "After removing odd numbers: ";
    for (const auto &num : numsInVec)
        cout << num << " ";
    cout << endl;

    return 0;
}

为什么每次调完remove() 都要调用erase() ?

因为remove() 函数中,在遍历时,它遍历一个范围内的元素。对于所有不等于你要移除的值的元素,它会 把它们移到范围的前面 。它返回一个迭代器,指向新的“逻辑上”的末尾。这个迭代器后面的元素是“垃圾”,它们仍然在容器中,但它们的值是不确定的,也不再属于有效序列。然后使用容器的 erase() 成员函数,它负责根据第一步返回的迭代器,真正地从容器中删除后面的元素,并调整容器大小。这一步是特定于容器的,所以很高效, 因为它只需要知道一个起点,就可以批量删除。

替换值以及替换满足给定条件的元素

STL算法 replace()replace_if() 分别用于替换集合中等于指定值和满足给定条件的元素。 replace() 根据比较运算符==的返回值来替换元素:

cout << "Using 'std::replace' to replace value 5 by 8" << endl;
replace (numsInVec.begin (), numsInVec.end (), 5, 8);

replace_if( )需要一个用户指定的一元谓词,对于要替换的每个值,该谓词都返回 true:

cout << "Using 'std::replace_if' to replace even values by -1" << endl;
replace_if (numsInVec.begin (), numsInVec.end (),
 [](int element) {return ((element % 2) == 0); }, -1); 

使用示例:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> numsInVec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    cout << "Original Vector: ";
    for (const auto &num : numsInVec)
        cout << num << " ";
    cout << endl;

    // Replace value 5 with 8
    replace(numsInVec.begin(), numsInVec.end(), 5, 8);

    cout << "After replacing 5 with 8: ";
    for (const auto &num : numsInVec)
        cout << num << " ";
    cout << endl;

    // Replace even values with -1
    replace_if(numsInVec.begin(), numsInVec.end(),
               [](int element) { return (element % 2) == 0; }, -1);

    cout << "After replacing even values with -1: ";
    for (const auto &num : numsInVec)
        cout << num << " ";
    cout << endl;

    return 0;
}

排序,有序搜索,去重

进行排序,可使用 STL 算法 sort( ):

sort (numsInVec.begin (), numsInVec.end ()); // ascending order

这个版本的 sort( )将 std::less<>用作二元谓词,而该谓词使用 vector 存储的数据类型实现的运算符<。您可使用另一个重载版本,以指定谓词,从而修改排列顺序:

sort (numsInVec.begin (), numsInVec.end (),
 [](int lhs, int rhs) {return (lhs > rhs);} ); // descending order

同样,在显示集合的内容前,需要删除重复的元素。要删除相邻的重复值,可使用 unique( ):

auto newEnd = unique (numsInVec.begin (), numsInVec.end ());
numsInVec.erase (newEnd, numsInVec.end ()); // to resize

要进行快速查找,可使用 STL 算法 binary_search( ),这种算法只能用于有序容器:

bool elementFound = binary_search (numsInVec.begin (), numsInVec.end (), 2011);
if (elementFound)
 cout << "Element found in the vector!" << endl; 

使用实例:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;


int main()
{
    vector<int> numsInVec = {5, 3, 8, 1, 2, 2, 7, 4, 6, 5, 5};

    cout << "Original Vector: ";
    for (const auto &num : numsInVec)
        cout << num << " ";
    cout << endl;

    // Sort the vector in ascending order
    sort(numsInVec.begin(), numsInVec.end());

    cout << "Sorted Vector (Ascending): ";
    for (const auto &num : numsInVec)
        cout << num << " ";
    cout << endl;

    // Remove duplicates
    auto newEnd = unique(numsInVec.begin(), numsInVec.end());
    numsInVec.erase(newEnd, numsInVec.end());

    cout << "After removing duplicates: ";
    for (const auto &num : numsInVec)
        cout << num << " ";
    cout << endl;

    // Search for an element
    bool elementFound = binary_search(numsInVec.begin(), numsInVec.end(), 4);
    if (elementFound)
        cout << "Element 4 found in the vector!" << endl;
    else
        cout << "Element 4 not found in the vector!" << endl;

    return 0;
}

请注意,与 remove() 一样, unique() 也不调整容器的大小。它将元素前移,但不会减少元素总数。为避免容器末尾包含不想要或未知的值,务必在调用 unique() 后调用 vector::erase() ,并将 unique() 返回的迭代器传递给它。

binary_search( )算法只能用于经过排序的容器,如果将其用于未经排序的 vector,结果可能出乎意料。

stable_sort( )的用法与 sort( )类似,这在前面介绍过。stable_sort( )确保排序后元素的相对顺序保持不变。为了确保相对顺序保持不变,将降低性能,这是一个需要考虑的因素,尤其在元素的相对顺序不重要时。

将范围分区

std::partition() 将输入范围分为两部分:一部分满足一元谓词;另一部分不满足:

bool IsEven (const int& num) // unary predicate
{ 
 return ((num % 2) == 0);
}
...
partition (numsInVec.begin(), numsInVec.end(), IsEven);

然而,std::partition( ) 不保证每个分区中元素的相对顺序不变。在相对顺序很重要,需要保持不变时,应使用 std::stable_partition( )

stable_partition (numsInVec.begin(), numsInVec.end(), IsEven);

使用举例:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> numsInVec = {5, 3, 8, 1, 2, 2, 7, 4, 6, 5, 5, 9};
    vector<int> vecCopy(numsInVec);

    cout << "Original vector numsInVec: ";
    for (const auto &num : numsInVec)
    {
        cout << num << " ";
    }
    cout << endl;

    cout << "Partitioned vector numsInVec: ";
    auto partitionedEnd = partition(numsInVec.begin(), numsInVec.end(), [](const int &a)
                                    { return ((a % 2) == 0); });

    cout << "Partition Left size: " << distance(numsInVec.begin(), partitionedEnd) << endl;
    cout << "Partition Right size: " << distance(partitionedEnd, numsInVec.end()) << endl;

    for (auto it = numsInVec.cbegin(); it != numsInVec.cend(); it++)
    {
        cout << *it << " ";
    }
    cout << endl;

    cout << "Stable Partitioned vector vecCopy: ";
    stable_partition(vecCopy.begin(), vecCopy.end(), [](const int &a)
                     { return ((a % 2) == 0); });

    for (auto it = vecCopy.cbegin(); it != vecCopy.cend(); it++)
    {
        cout << *it << " ";
    }
    cout << endl;

    return 0;
}

partition() 会返回一个迭代器,指向第一个不满足谓词的元素。就这个例子来说,其指向的就是 1 这个元素。

          丅
6 4 8 2 2 1 7 3 5 5 5 9 

stable_partition() 可以保证元素分区后相对位置不变,其速度比 partition() 慢,因此应只在容器中元素的相对顺序很重要时使用它。

在有序集合中插入元素

将元素插入到有序集合中时,将其插入到正确位置很重要。为了满足这种需求,STL 提供了 std::lower_bound( )std::upper_bound( ) 等函数:

auto minInsertPos = lower_bound (names.begin(), names.end(),
 "Brad Pitt");
// alternatively:
auto maxInsertPos = upper_bound (names.begin(), names.end(),
 "Brad Pitt");

lower_bound()upper_bound() 都返回一个迭代器,分别指向在不破坏现有顺序的情况下,元素可插入到有序范围内的最前位置和最后位置。

例如,要将 2 插入到1222345中,lower_bound() 会返回指向第一个 2 的迭代器,而 upper_bound() 会返回指向 3 的迭代器。

使用举例:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> numsInVec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Insert an element while maintaining order
    int elementToInsert = 5;
    auto insertPos = lower_bound(numsInVec.begin(), numsInVec.end(), elementToInsert);

    numsInVec.insert(insertPos, elementToInsert);

    cout << "After inserting " << elementToInsert << ": ";
    for (const auto &num : numsInVec)
        cout << num << " ";
    cout << endl;

    return 0;
}

STL 算法小结

  • 使用算法 remove()remove_if()unique() 后,务必使用容器的成员方法 erase() 调整容器的大小。
  • 调用 unique() 删除重复的相邻值之前,别忘了使用 sort() 对容器进行排序。sort() 确保包含相同值的元素彼此相邻,这样 unique() 才能发挥作用。
  • 使用 find()find_if()search()search_n() 返回的迭代器之前,务必将其与容器的 end() 进行比较,以确定它有效。如果迭代器等于 end() ,则表示未找到元素。
  • 仅当元素的相对顺序很重要时,才应使用 stable_partition() (而不是 partition() )和 stable_sort() (而不是 sort() ),因为 stable_* 可能降低应用程序的性能。
  • 对于已排序的容器,不要在随机选择的位置插入元素,而应将其插入到 lower_bound()upper_bound() 返回的位置,以确保插入元素后容器依然是有序的。
  • 别忘了,binary_search() 只能用于已排序的容器。

栈与队列

栈是 LIFO(后进先出)系统,只能从栈顶插入或删除元素。栈的操作包括 push() (插入)和 pop() (删除)。要使用 std::stack,必须包含头文件 <stack>

队列是 FIFO(先进先出)系统,只能从队头删除元素,从队尾插入元素。队列的操作包括 enqueue() (插入)和 dequeue() (删除)。要使用 std::queue,必须包含头文件<queue>

栈的使用

在有些 STL 实现中,std::stack 的定义如下:

template <
 class elementType,
 class Container=deque<Type>
> class stack;

参数 elementType 是 stack 存储的对象类型。第二个模板参数 Container 是 stack 使用的默认底层容器实现类。stack 默认在内部使用 std::deque 来存储数据,但 可指定使用 vector 或 list 来存储数据 。因此,实例化整型栈的代码类似于下面这样:

std::stack <int> numsInStack;

要创建存储类(如 Tuna)对象的栈,可使用下述代码:

std::stack <Tuna> tunasInStack;

要创建使用不同底层容器的栈,可使用如下代码:

std::stack <double, vector <double>> doublesStackedInVec; 

各种实例化方式:

#include <iostream>
#include <stack>
#include <vector>
using namespace std;

int main()
{
    // Stack of integers using default deque container
    stack<int> intStack;

    // Stack of strings using default deque container
    stack<string> stringStack;

    // Stack of doubles using vector as the underlying container
    stack<double, vector<double>> doubleStack;

    // initializing one stack to be a copy of another
    stack<int> intStackTwo(intStack);

    return 0;
}

stack常用成员函数

stack 改变了另一种容器(如 deque、list 或 vector)的行为,通过 限制元素插入或删除的方式 实现其功能,从而提供严格遵守栈机制的行为特征。

成员函数说明
empty()如果栈为空,返回 true;否则返回 false。
size()返回栈中元素的数量。
top()返回栈顶元素的引用。
push(const Type& x)将元素 x 插入到栈顶。
pop()删除栈顶元素。

使用举例:

#include <iostream>
#include <stack>
using namespace std;

int main()
{
    stack<int> numsInStack;

    // Push elements onto the stack
    for (int i = 1; i <= 5; ++i)
    {
        numsInStack.push(i);
        cout << "Pushed: " << i << ", Stack size: " << numsInStack.size() << endl;
    }

    // Access and pop elements from the stack
    while (!numsInStack.empty())
    {
        cout << "Top element: " << numsInStack.top() << endl;
        numsInStack.pop();
        cout << "Popped, Stack size: " << numsInStack.size() << endl;
    }

    return 0;
}

queue队列的使用

STL queue 是一个模板类,要使用它,必须包含头文件。queue 是一个泛型类,只允许在末尾插入元素以及从开头删除元素。queue 不允许访问中间的元素,但可以访问开头和末尾的元素。从这种意义上说,std::queue 的行为与超市收银台前的队列极其相似。

std::queue 的定义如下:

template <
 class elementType,
 class Container = deque<Type>
> class queue;

其中 elementType 是 queue 对象包含的元素的类型。Container 是 std::queue 用于存储其数据的集合类型,可将该模板参数设置为 std::list、vector 或 deque,默认为 deque。

实例化整型 queue 的最简单方式如下:

std::queue <int> numsInQ;

如果要创建这样的 queue,即其元素类型为 double,并使用 std::list(而不是默认的 queue)存储这些元素,可以像下面这样做:

std::queue <double, list <double>> dblsInQInList;

与 stack 一样,也可使用一个 queue 来实例化另一个 queue:

std::queue<int> copyQ(numsInQ);

queue常用成员函数

有以下这些成员函数:

成员函数说明
empty()如果队列为空,返回 true;否则返回 false。
size()返回队列中元素的数量。
front()返回队列头元素的引用。
back()返回队列尾元素的引用。
push(const Type& x)将元素 x 插入到队列尾。
pop()删除队列头元素。

使用举例:

#include <iostream>
#include <queue>
using namespace std;

int main()
{
    queue<int> numsInQ;

    // Enqueue elements into the queue
    for (int i = 1; i <= 5; ++i)
    {
        numsInQ.push(i);
        cout << "Enqueued: " << i << ", Queue size: " << numsInQ.size() << endl;
    }

    // Access and dequeue elements from the queue
    while (!numsInQ.empty())
    {
        cout << "Front element: " << numsInQ.front() << endl;
        numsInQ.pop();
        cout << "Dequeued, Queue size: " << numsInQ.size() << endl;
    }

    return 0;
}

priority_queue优先级队列

std::priority_queue 是一个特殊类型的队列,它允许你按照优先级来处理元素。它的行为类似于一个最大堆(max-heap),即每次从队列中取出的元素都是当前队列中最大的元素。你可以使用 std::priority_queue 来实现优先队列,例如任务调度、事件处理等。

std::priority_queue 类的定义如下:

template <
 class elementType,
 class Container=vector<Type>,
 class Compare=less<typename Container::value_type>
>
class priority_queue

其中 elementType 是一个模板参数,指定了优先级队列将包含的元素的类型。第二个模板参数指定 priority_queue 在内部将使用哪个集合类来存储数据,第三个参数让程序员能够指定一个二元谓词,以帮助队列判断哪个元素应位于队首。如果没有指定二元谓词,priority_queue 类将默认使用 std::less,它使用运算符<比较对象。

要实例化整型 priority_queue,最简单的方式如下:

std::priority_queue <int> numsInPrioQ;

如果要创建一个这样的 priority_queue,即其元素类型为 double,且按小到大的顺序存储在 std::deque中,则可这样做:

priority_queue <int, deque <int>, greater <int>> numsInDescendingQ;

与 stack 一样,也可使用一个 priority_queue 来实例化另一个 priority_queue:

std::priority_queue <int> copyQ(numsInPrioQ); 

priority_queue常用成员函数

有以下这些成员函数:

成员函数说明
empty()如果队列为空,返回 true;否则返回 false。
size()返回队列中元素的数量。
top()返回队列头元素的引用。
push(const Type& x)将元素 x 插入到队列尾。
pop()删除队列头元素。

使用举例:

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

int main()
{
    priority_queue<int> pq;

    vector<int> vec = {5, 1, 3, 2, 4};

    for (int num : vec)
    {
        pq.push(num);
    }

    while (!pq.empty())
    {
        cout << pq.top() << endl;
        pq.pop();
    }

    return 0;
}

按照51324的顺序插入之后,使用top获取后弹出,打印顺序为54321。可以得知,priority_queue 默认实例里,每次取出的元素都是当前队列中最大的元素。

使用谓词 std::greater <int> 实例化一个 priority_queue。该谓词导致优先级队列认为包含的数字最小的元素为最大的元素,并将其放在队首。

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

int main()
{
    priority_queue<int, vector<int>, greater<int>> pq;

    vector<int> vec = {5, 1, 3, 2, 4};

    for (int num : vec)
    {
        pq.push(num);
    }

    while (!pq.empty())
    {
        cout << pq.top() << endl;
        pq.pop();
    }

    return 0;
}

【C++】C++基础记录(一)

【C++】C++基础记录(一)

本文记录了我学习C++的一些基础条目知识

HelloWorld

#include <iostream>

int main()
{
    std::cout << "Hello World" << std::endl;
    return 0;
}

命名空间

名称空间是给代码指定的名称,有助于降低命名冲突的风险。通过使用 std::cout,您命令编译器调用名称空间 std 中独一无二的 cout。

// Pre-processor directive
#include <iostream>

// Start of your program
int main()
{
   // Tell the compiler what namespace to look in
   using namespace std;

   /* Write to the screen using cout */
   cout << "Hello World" << endl;

   // Return a value to the OS
   return 0;
}

使用函数之前需要声明

#include <iostream>
using namespace std;

// Function declaration
int DemoConsoleOutput();

int main()
{
   // Function call
    DemoConsoleOutput();

    return 0;
}

// Function definition
int DemoConsoleOutput()
{
    cout << "This is a simple string literal" << endl;
    cout << "Writing number five: " << 5 << endl;
    cout << "Performing division 10 / 5 = " << 10 / 5 << endl;
    cout << "Pi when approximated is 22 / 7 = " << 22 / 7 << endl;
    cout << "Pi actually is 22 / 7 = " << 22.0 / 7 << endl;

    return 0;
}

使用变量最好初始化

int firstNumber = 0;

除非给变量赋初值,否则无法确保相应内存单元的内容是什么,这对程序可能不利。因此,初始化虽然是可选的,但这样做通常是一个不错的编程习惯。

常见变量类型

基本类型(基本数据类型)

‌整型(Integer types)

int:标准整型,通常是32位。 short:短整型,通常是16位。 long:长整型,通常是32位或64位,取决于系统。 long long:更长整型,通常是64位。 unsigned:无符号整型,可以是unsigned int、unsigned short等。

‌字符型(Character types)

char:字符类型,通常是8位。 signed char:有符号字符类型。 unsigned char:无符号字符类型。

‌浮点型(Floating-point types)

float:单精度浮点型,通常是32位。 double:双精度浮点型,通常是64位。 long double:扩展精度浮点型,通常是80位或更高。

‌布尔型(Boolean type)‌

bool:布尔类型,可以存储true或false。

auto可以自动推断类型

#include <iostream>
using namespace std;

int main()
{
   auto coinFlippedHeads = true;
   auto largeNumber = 2500000000000;

   cout << "coinFlippedHeads = " << coinFlippedHeads;
   cout << " , sizeof(coinFlippedHeads) = " << sizeof(coinFlippedHeads) << endl;
   cout << "largeNumber = " << largeNumber;
   cout << " , sizeof(largeNumber) = " << sizeof(largeNumber) << endl;

   return 0;
}

/**
coinFlippedHeads = 1 , sizeof(coinFlippedHeads) = 1
largeNumber = 2500000000000 , sizeof(largeNumber) = 8 
*/

typedef更改变量类型别名

C++允许您将变量类型替换为您认为方便的名称,为此可使用关键字 typedef。

在下面的示例中,程序员想给 unsigned int 指定一个更具描述性的名称— STRICTLY_POSITIVE_INTEGER

typedef unsigned int STRICTLY_POSITIVE_INTEGER;
STRICTLY_POSITIVE_INTEGER numEggsInBasket = 4532;

常量const和constexpr

如果变量的值不应改变,就应将其声明为常量,这是一种良好的编程习惯。通过使用关键字 const,程序员可确保数据不变,避免应用程序无意间修改该常量。在多位程序员合作开发时,这特别有用。

通过constexpr将函数声明为返回常量的函数:

constexpr double GetPi() {return 22.0 / 7;}

会在编译期就算出这个值,并在使用处自动替换,可以优化性能。

但是像计算用户输入数字的两倍,这种地方,就无法计算结果,不保证可以优化性能。

枚举

后一个都比前一个大1。默认第一个数值从0开始,中间也可以自己指定。

#include <iostream>
using namespace std;

enum CardinalDirections
{
   North = 25,
   South,
   East,
   West
};

int main()
{
    cout << "Displaying directions and their symbolic values" << endl;
    cout << "North: " << North << endl;
    cout << "South: " << South << endl;
    cout << "East: " << East << endl;
    cout << "West: " << West << endl;

   CardinalDirections windDirection = South;
   cout << "Variable windDirection = " << windDirection << endl;

   return 0;
}

#define pi 3.14286用来定义常量,已废弃

数组声明和访问元素

#include <iostream>

using namespace std;

int main ()
{
   int myNumbers [5] = {34, 56, -21, 5002, 365};

   cout << "First element at index 0: " << myNumbers [0] << endl;
   cout << "Second element at index 1: " << myNumbers [1] << endl;
   cout << "Third element at index 2: " << myNumbers [2] << endl;
   cout << "Fourth element at index 3: " << myNumbers [3] << endl;
   cout << "Fifth element at index 4: " << myNumbers [4] << endl;

   return 0;
}

多维数组

#include <iostream>
using namespace std;

int main()
{
   int threeRowsThreeColumns [3][3] = { {-501, 205, 2011}, {989, 101, 206}, {303, 456, 596} };
   
   cout << "Row 0: " << threeRowsThreeColumns [0][0] << " " << threeRowsThreeColumns [0][1] << " " << threeRowsThreeColumns [0][2] << endl;

   cout << "Row 1: " << threeRowsThreeColumns [1][0] << " " << threeRowsThreeColumns [1][1] << " " << threeRowsThreeColumns [1][2] << endl;

   cout << "Row 2: " << threeRowsThreeColumns [2][0] << " " << threeRowsThreeColumns [2][1] << " " << threeRowsThreeColumns [2][2] << endl;

   return 0;
}

使用vector声明动态数组

#include <iostream>
#include <vector>

using namespace std;

int main()
{
   vector<int> dynArray (3);

   dynArray[0] = 365;
   dynArray[1] = -421;
   dynArray[2]= 789;

   cout << "Number of integers in array: " << dynArray.size() << endl;

   cout << "Enter another element to insert" << endl;
   int newValue = 0;
   cin >> newValue;
   dynArray.push_back(newValue);

   cout << "Number of integers in array: " << dynArray.size() << endl;
   cout << "Last element in array: ";
   cout << dynArray[dynArray.size() - 1] << endl;
   
   return 0;
}

c风格字符串

char sayHello[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd',
'\0'};
std::cout << sayHello << std::endl;

末尾是一个 \0 ,来告诉编译器字符串到这里就完结了。

危险性

使用 C 语言编写的应用程序经常使用 strcpy() 等字符串复制函数、strcat() 等拼接函数,还经常使用 strlen() 来确定字符串的长度;具有 C 语言背景的 C++程序员编写的应用程序亦如此。

这些 C 风格字符串作为输入的函数非常危险,因为它们寻找终止空字符,如果程序员没有在字符数组末尾添加空字符,这些函数将跨越字符数组的边界。

C++ string

要使用 C++字符串,需要包含头文件 string, #include <string> 不同于字符数组(C 风格字符串实现),std::string 是动态的,在需要存储更多数据时其容量将增大。

#include <iostream>
#include <string>

using namespace std;

int main()
{
   string greetStrings ("Hello std::string!");
   cout << greetStrings << endl;

   cout << "Enter a line of text: " << endl;
   string firstLine;
   getline(cin, firstLine);

   cout << "Enter another: " << endl;
   string secondLine;
   getline(cin, secondLine);

   cout << "Result of concatenation: " << endl;
   string concatString = firstLine + " " + secondLine;
   cout << concatString << endl;

   cout << "Copy of concatenated string: " << endl;
   string aCopy;
   aCopy = concatString;
   cout << aCopy << endl;

   cout << "Length of concat string: " << concatString.length() << endl;

   return 0;
}

函数值传递

默认情况下, 函数参数在作用域生效的是外部实参的拷贝 ,内部的操作不会影响原参数。

可以使用按引用传递的参数,在函数体内部也可以对外部传来的参数做修改。

#include <iostream>
using namespace std;

const double Pi = 3.1416;

// output parameter result by reference 
void Area(double radius, double& result)
{
   result = Pi * radius * radius;
}

int main() 
{
   cout << "Enter radius: ";
   double radius = 0;
   cin >> radius;

   double areaFetched = 0;
   Area(radius, areaFetched);

   cout << "The area is: " << areaFetched << endl;
   return 0;
}

函数调用栈的概念

栈是一种后进先出的内存结构,很像堆叠在一起的盘子,您从顶部取盘子,这个盘子是最后堆叠上去的。将数据加入栈被称为压入操作;从栈中取出数据被称为弹出操作。栈增大时,栈指针将不断递增,始终指向栈顶。

栈的性质使其非常适合用于处理函数调用。函数被调用时,所有局部变量都在栈中实例化,即被压入栈中。函数执行完毕时,这些局部变量都从栈中弹出,栈指针返回到原来的地方。

inline函数

也叫内联函数,函数执行时间入栈出栈时间 相当的简单函数,可以使用inline关键字,编译器会直接将其展开到调用处,省去入栈出栈的时间。

现代编译器甚至会自动寻找内联机会,帮助合理优化性能。

auto用于函数,同样可以自动推断返回值

指针初始化,以免自动指向垃圾值

与大多数变量一样,除非对指针进行初始化,否则它包含的值将是随机的。

您不希望访问随机的内存地址,因此将指针初始化为 NULL

NULL 是一个可以检查的值,且不会是内存地址。

指针语法

声明一个指针类型,使用 指向的变量类型加上星号(*) ,后跟指针名称。

int* pointsToInt;

使用取地址运算符(&)获取变量的内存地址,取地址运算符是编程中用于获取变量在内存中的地址的符号,通常用符号&表示。它在支持指针操作的编程语言中广泛使用,功能是返回变量的内存地址,以便后续操作。

#include <iostream>
using namespace std;

int main()
{
   int age = 30;

   int* pointsToInt = &age;
   cout << "pointsToInt points to age now" << endl;

   // Displaying the value of pointer
   cout << "pointsToInt = 0x" << hex << pointsToInt << endl;

   int DogsAge = 9;
   pointsToInt = &DogsAge;
   cout << "pointsToInt points to DogsAge now" << endl;

   cout << "pointsToInt = 0x" << hex << pointsToInt << endl;

   return 0;
}

使用解引用运算符(*)获取指针指向的数据值

解引用运算符是编程中用于访问指针所指向内存地址存储值的符号,通常用星号(*)表示‌。它在支持指针操作的编程语言如C语言和C++中广泛使用,主要功能是通过指针间接访问和修改数据。

其核心工作原理是:当对指针使用解引用运算符(如ptr)时,会获取该指针指向内存位置的实际值。例如,若指针int ptr指向整数变量num的地址,则*ptr操作等同于直接操作num的值。

与取地址运算符(&)形成互补关系:取地址运算符用于获取变量的内存地址(如&num返回地址),而解引用运算符则通过地址反向获取值。在面向对象编程中,解引用运算符还用于通过指针访问对象的成员,例如obj->member等价于(*obj).member,其中->被称为结构解引用运算符,是解引用与成员访问的语法糖。

使用时的注意事项包括:

  • ‌空指针解引用风险‌:若指针未初始化或指向无效内存(如空指针),解引用会导致程序崩溃或未定义行为;
  • ‌类型匹配‌:解引用指针类型需与目标数据类型一致,否则可能引发内存错误;
  • ‌运算符优先级‌:解引用运算符(*)的优先级高于自增(++),因此*p++会先解引用再对指针自增,而(*p)++则是对解引用的值自增。

典型应用场景包括动态内存管理、数据结构(如链表)操作及数组遍历。例如,通过指针遍历数组时,*(arr + i)等效于arr[i],这展示了指针算术与解引用的结合。

#include <iostream>
using namespace std;

int main()
{
   int age = 30;
   int dogsAge = 9;

   cout << "Integer age = " << age << endl;
   cout << "Integer dogsAge = " << dogsAge << endl;

   int* pointsToInt = &age;
   cout << "pointsToInt points to age" << endl;

   // Displaying the value of pointer
   cout << "pointsToInt = 0x" << hex << pointsToInt << endl;

   // Displaying the value at the pointed location
   cout << "*pointsToInt = " << dec << *pointsToInt << endl;

   pointsToInt = &dogsAge;
   cout << "pointsToInt points to dogsAge now" << endl;

   cout << "pointsToInt = 0x" << hex << pointsToInt << endl;
   cout << "*pointsToInt = " << dec << *pointsToInt << endl;

   return 0;
}

如果指针未初始化,它所在的内存单元将包含随机值,此时对其解除引用通常会导致非法访问(Access Violation),即访问应用程序未获得授权的内存单元。

使用new和delete动态的申请和释放内存

这两个必须成对使用。

不再使用分配的内存后,如果不释放它们,这些内存仍被预留并分配给您的应用程序。这将减少可供其他应用程序使用的系统内存量,甚至降低您的应用程序的执行速度。这被称为 内存泄露 ,应不惜一切代价避免这种情况发生。

#include <iostream>
using namespace std;

int main()
{
   // Request for memory space for an int
   int* pointsToAnAge = new int;

   // Use the allocated memory to store a number
   cout << "Enter your dog's age: ";
   cin >> *pointsToAnAge;

   // use indirection operator* to access value 
   cout << "Age " << *pointsToAnAge << " is stored at 0x" << hex << pointsToAnAge << endl;

   delete pointsToAnAge; // release dynamically allocated memory

   return 0;
}

不能将运算符 delete 用于任何包含地址的指针,而只能用于 new 返回的且未使用 delete 释放的指针。

对指针使用++和–

其会指向下一个int值,而不是移动一个内存地址指向中间,那毫无意义。 如果声明了如下指针:

Type* pType = Address;

则执行 ++pType 后, pType 将指向 Address + sizeof(Type)

可以推断,数组其实就是一个指向第一个元素的指针类型。

数组和指针

由于数组变量就是指针,因此也可将用于指针的解除引用运算符(*)用于数组。同样,可将数组运算符[ ]用于指针。

#include <iostream>
using namespace std;

int main()
{
   const int ARRAY_LEN = 5;

   // Static array of 5 integers, initialized
   int myNumbers[ARRAY_LEN] = {24, -1, 365, -999, 2011};

   // Pointer initialized to first element in array
   int* pointToNums = myNumbers;

   cout << "Display array using pointer syntax, operator*" << endl;
   for (int index = 0; index < ARRAY_LEN; ++index)
      cout << "Element " << index << " = " << *(myNumbers + index) << endl;

   cout << "Display array using ptr with array syntax, operator[]" << endl;
   for (int index = 0; index < ARRAY_LEN; ++index)
      cout << "Element " << index << " = " << pointToNums[index] << endl;

   return 0;
}

const指针

指针的主要功能是指向一个 变量的地址 ,理解为越靠近变量的限制性越高级。

  • const贴近变量名,不允许修改指针指向的地址,可以修改指向的变量。
#include <iostream>
using namespace std;

int main()  
{
   int age = 30;
   int dogsAge = 9;

   int* const pointsToAge = &age;
   cout << "*pointsToAge = " << *pointsToAge << endl;

   // pointsToAge = &dogsAge; // error! can't change pointer value
   *pointsToAge = 31; // ok! can change value pointed to
   cout << "*pointsToAge = " << *pointsToAge << endl;

   return 0;
}
  • const远离变量名,在最前,不允许修改指向变量的值,指针可以指向其他地方。
#include <iostream>
using namespace std;

int main()
{
   int age = 30;
   int dogsAge = 9;

   const int* pointsToAge = &age;
   cout << "*pointsToAge = " << *pointsToAge << endl;

   // *pointsToAge = 31; // error! can't change value pointed to
   pointsToAge = &dogsAge; // ok! can change pointer value
   cout << "*pointsToAge = " << *pointsToAge << endl;

   return 0; 
}
  • 两个都有 const 修饰,则都不可以修改。
#include <iostream>
using namespace std;

int main()
{
   int age = 30;
   int dogsAge = 9;

   const int* const pointsToAge = &age;
   cout << "*pointsToAge = " << *pointsToAge << endl;

   // *pointsToAge = 31; // error! can't change value pointed to
   // pointsToAge = &dogsAge; // error! can't change pointer value

   return 0; 
}

指针传递给函数

需要提前规定好 函数内部可以修改哪些值,不可以修改哪些值

#include <iostream>
using namespace std;

void CalcArea(const double* const ptrPi, // const pointer to const data
              const double* const ptrRadius, // i.e. no changes allowed
              double* const ptrArea)  // can change data pointed to,but not pointer
{
   // check pointers for validity before using!
   if (ptrPi && ptrRadius && ptrArea) 
      *ptrArea = (*ptrPi) * (*ptrRadius) * (*ptrRadius);
}

int main()
{
   const double Pi = 3.1416;

   cout << "Enter radius of circle: ";
   double radius = 0;
   cin >> radius;

   double area = 0;
   CalcArea (&Pi, &radius, &area);

   cout << "Area is = " << area << endl;

   return 0;
}

使用new的内存分配请求可能失败

为防止报错,此时使用 try-catch 或者使用 new(nothrow) ,它在分配内存失败时返回 NULL 。也不会报错退出。

  • try catch
#include <iostream>
using namespace std;

// remove the try-catch block to see this application crash 
int main()
{
   try
   {
      // Request a LOT of memory!
      int* pointsToManyNums = new int [0x1fffffff];

      // Use the allocated memory 

      delete[] pointsToManyNums;
   }
   catch (bad_alloc)
   {
      cout << "Memory allocation failed. Ending program" << endl;
   }
   return 0;
}
  • 使用new(nothrow)
#include <iostream>
using namespace std;

int main()
{
   // Request LOTS of memory space, use nothrow 
   int* pointsToManyNums = new(nothrow) int [0x1fffffff];

   if (pointsToManyNums) // check pointsToManyNums != NULL
   {
      // Use the allocated memory 
      delete[] pointsToManyNums;
   }
   else 
      cout << "Memory allocation failed. Ending program" << endl;

   return 0;
}

引用&是变量的别名

引用是变量的别名。声明引用时,需要将其初始化为一个变量,因此引用只是另一种访问相应变量存储的数据的方式。

#include <iostream>
using namespace std;

int main()
{
   int original = 30;
   cout << "original = " << original << endl;
   cout << "original is at address: " << hex << &original << endl;

   int& ref1 = original;
   cout << "ref1 is at address: " << hex << &ref1 << endl;

   int& ref2 = ref1;
   cout << "ref2 is at address: " << hex << &ref2 << endl;
   cout << "Therefore, ref2 = " << dec << ref2 << endl;

   return 0;
}

他们指向同一个地址。

引用的用处

函数参数,如果在合适的时机,按引用传递,可以省去变量复制的步骤,优化性能。

#include <iostream>
using namespace std;

void GetSquare(int& number)
{
   number *= number;
}

int main()
{
   cout << "Enter a number you wish to square: ";
   int number = 0;
   cin >> number;

   GetSquare(number);
   cout << "Square is: " << number << endl;

   return 0;
}

const用于引用

通过引用可以修改变量的值。可能需要禁止通过引用修改它指向的变量的值,为此可在声明引用时使用关键字 const。

int original = 30;
const int& constRef = original;
constRef = 40; // Not allowed: constRef can’t change value in original
int& ref2 = constRef; // Not allowed: ref2 is not const
const int& constRef2 = constRef; // OK

结合上一个,传参时,使用const引用,又可以避免复制,又可以确保函数体中,不可以修改按引用传进去的变量的值

声明和使用类

#include <iostream>
#include <string>
using namespace std;

class Human
{
public:
   string name;
   int age;

   void IntroduceSelf()
   {
      cout << "I am " + name << " and am ";
      cout << age << " years old" << endl;
   }
};

int main()
{
   // An object of class Human with attribute name as "Adam"
   Human firstMan;
   firstMan.name = "Adam";
   firstMan.age = 30;

   // An object of class Human with attribute name as "Eve"
   Human firstWoman;
   firstWoman.name = "Eve";
   firstWoman.age = 28;
   
   firstMan.IntroduceSelf();
   firstWoman.IntroduceSelf();
}

使用指针运算符访问成员

如果对象是使用 new 在自由存储区中实例化的,或者有指向对象的指针,则可使用指针运算符 -> 来访问成员属性和方法:

Human* firstWoman = new Human();
firstWoman->dateOfBirth = "1970";
firstWoman->IntroduceSelf();
delete firstWoman;

类中的变量和函数,如果未标明,默认都为 private ,外部不可以访问 默认构造函数:不用传参的构造函数,并非仅指无参构造函数。

构造函数可以重载,也可以设置成必须要初始化参数,还可以带默认参数 带初始化列表的构造函数。

写法如下:

#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
   int age;
   string name;

public:
   Human(string humansName = "Adam", int humansAge = 25)
        :name(humansName), age(humansAge)
   {
      cout << "Constructed a human called " << name;
      cout << ", " << age << " years old" << endl;
   }
};

int main()
{
   Human adam;
   Human eve("Eve", 18);

   return 0;
}

析构函数

用于回收资源,当类中有动态申请内存的操作时,一般需要设计析构函数,在类销毁时释放内存,声明为 ~ClassName() {}

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
   char* buffer;

public:
   MyString(const char* initString)  // constructor
   {
      if(initString != NULL)
      {
         buffer = new char [strlen(initString) + 1];
         strcpy(buffer, initString);
      }
      else 
         buffer = NULL;
   }

   ~MyString()
   {
      cout << "Invoking destructor, clearing up" << endl;
      if (buffer != NULL)
         delete [] buffer;
   }

   int GetLength() 
   {
      return strlen(buffer);
   }

   const char* GetString()
   {
       return buffer;
   }
};

int main()
{
   MyString sayHello("Hello from String Class");
   cout << "String buffer in sayHello is " << sayHello.GetLength();
   cout << " characters long" << endl;

   cout << "Buffer contains: " << sayHello.GetString() << endl;
}

类的浅复制的问题

把类当作参数传递给函数时,使用值传递模式,其会被复制。

如果其内部有指针指向的new出来的缓冲区,复制时会复制指针成员,但是不会复制一份缓冲区,在函数结束时,回收这个复制出来的类对象,删除掉了缓冲区,导致外部的类指向无效的内存地址。在外部类生命周期完结时,delete无效内存会报错。

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
   char* buffer;

public:
   MyString(const char* initString) // Constructor
   {
      buffer = NULL;
      if(initString != NULL)
      {
         buffer = new char [strlen(initString) + 1];
         strcpy(buffer, initString);
      }
   }

   ~MyString() // Destructor
   {
      cout << "Invoking destructor, clearing up" << endl;
      delete [] buffer;
   }

   int GetLength() 
   { return strlen(buffer); }

   const char* GetString()
   { return buffer; }
};

void UseMyString(MyString str)
{
   cout << "String buffer in MyString is " << str.GetLength();
   cout << " characters long" << endl;

   cout << "buffer contains: " << str.GetString() << endl;
   return;
}

int main()
{
   MyString sayHello("Hello from String Class");
   UseMyString(sayHello); 

   return 0;
}

/**
String buffer in MyString is 23 characters long
buffer contains: Hello from String Class
Invoking destructor, clearing up
Invoking destructor, clearing up
<crash as seen in Figure 9.2>
*/

复制构造函数

一个专门的用于复制流程的构造函数,当通过 = 传递来复制类,或者当作函数参数来复制时。编译器会自动调用这个构造函数来生成一个新的对象。

默认格式为,传入一个 const引用 的构造函数,借用这个外部对象的数据来重新构造一个新的复制对象。

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
   char* buffer;

public:
   MyString() {}
   MyString(const char* initString) // constructor
   {
      buffer = NULL;
      cout << "Default constructor: creating new MyString" << endl;
      if(initString != NULL)
      {
         buffer = new char [strlen(initString) + 1];
         strcpy(buffer, initString);

         cout << "buffer points to: 0x" << hex;
         cout << (unsigned int*)buffer << endl;
      }
   }

   MyString(const MyString& copySource) // Copy constructor
   {
      buffer = NULL;
      cout << "Copy constructor: copying from MyString" << endl;
      if(copySource.buffer != NULL)
      {
         // allocate own buffer 
         buffer = new char [strlen(copySource.buffer) + 1];

         // deep copy from the source into local buffer
         strcpy(buffer, copySource.buffer);

         cout << "buffer points to: 0x" << hex;
         cout << (unsigned int*)buffer << endl;
      }
   }

   MyString operator+ (const MyString& addThis) 
   {
      MyString newString;

      if (addThis.buffer != NULL)
      {
         newString.buffer = new char[GetLength() + strlen(addThis.buffer) + 1];
         strcpy(newString.buffer, buffer);
         strcat(newString.buffer, addThis.buffer);
      }

      return newString;
   }

   // Destructor
   ~MyString()
   {
      cout << "Invoking destructor, clearing up" << endl;
      delete [] buffer;
   }

   int GetLength() 
   { return strlen(buffer); }

   const char* GetString()
   { return buffer; }
};

void UseMyString(MyString str)
{
   cout << "String buffer in MyString is " << str.GetLength();
   cout << " characters long" << endl;

   cout << "buffer contains: " << str.GetString() << endl;
   return;
}

int main()
{
   MyString sayHello("Hello from String Class");
   UseMyString(sayHello);

   return 0;
}

将复制构造函数和 = 运算符覆写成私有的

这个类不允许复制操作,编译时就会提示。

单例类

进一步将构造函数设置私有,提供一个static函数,返回一个static对象的引用,就是一个单例类,禁止复制,赋值,创建多实例。static对象只会创建一次,全局均可访问。所有的地方调用的都是这同一个实例。

#include <iostream>
#include <string>
using namespace std;

class President
{
private:
   President() {}; // private default constructor
   President(const President&); // private copy constructor
   const President& operator=(const President&); // assignment operator

   string name;

public:
   static President& GetInstance()
   {
      // static objects are constructed only once
      static President onlyInstance; 
      return onlyInstance;
   }

   string GetName()
   { return name; }

   void SetName(string InputName)
   { name = InputName; }
};

int main()
{
   President& onlyPresident = President::GetInstance();
   onlyPresident.SetName("Abraham Lincoln");

   // uncomment lines to see how compile failures prohibit duplicates
   // President second; // cannot access constructor
   // President* third= new President(); // cannot access constructor
   // President fourth = onlyPresident; // cannot access copy constructor
   // onlyPresident = President::GetInstance(); // cannot access operator=

   cout << "The name of the President is: ";
   cout << President::GetInstance().GetName() << endl;

   return 0;
}

将析构函数设为私有,就禁止在栈中实例化

只能通过 new 关键字,在自由存储区实例化

#include <iostream>
using namespace std;

class MonsterDB 
{
private:
   ~MonsterDB() {}; // private destructor prevents instances on stack

public:
   static void DestroyInstance(MonsterDB* pInstance)
   {
      delete pInstance; // member can invoke private destructor
   }

   void DoSomething() {} // sample member method
};

int main()
{
   MonsterDB* myDB = new MonsterDB(); // on heap
   myDB->DoSomething();

   // uncomment next line to see compile failure 
   // delete myDB; // private destructor cannot be invoked

   // use static member to release memory
   MonsterDB::DestroyInstance(myDB);

   return 0;
}

隐式转换和预防

Human类构造函数接受一个int类型作为参数。

这样的转换构造函数让您能够执行隐式转换:

Human anotherKid = 11; // int converted to Human
DoSomething(10); // 10 converted to Human!

函数 DoSomething(Human person)被声明为接受一个 Human(而不是 int)参数!前面的代码为何可行呢?这是因为编译器知道 Human 类包含一个将整数作为参数的构造函数,进而替您执行了隐式转换:将您提供的整数作为参数发送给这个构造函数,从而创建一个Human 对象。

使用 explicit 关键字避免隐式转换:

#include<iostream>
using namespace std;

class Human
{
   int age;
public:
   // explicit constructor blocks implicit conversions
   explicit Human(int humansAge) : age(humansAge) {}
};

void DoSomething(Human person)
{
   cout << "Human sent did something" << endl;
   return;
}

int main()
{
   Human kid(10);    // explicit converion is OK
   Human anotherKid = Human(11); // explicit, OK
   DoSomething(kid); // OK

   // Human anotherKid = 11; // failure: implicit conversion not OK
   // DoSomething(10); // implicit conversion 

   return 0;
}

this指针

当您在类成员方法中调用其他成员方法时,编译器将隐式地传递 this 指针—函数调用中不可见的参数:

class Human
{
private:
 void Talk (string Statement)
 {
 cout << Statement;
 }
public:
 void IntroduceSelf()
 {
 Talk("Bla bla"); // same as Talk(this, "Bla Bla")
 }
};

在这里,方法 IntroduceSelf( )使用私有成员 Talk( )在屏幕上显示一句话。实际上,编译器将在调用Talk 时嵌入 this 指针,即:

Talk(this, Blab la)

sizeof()用于类

在这种情况下,它将指出类声明中所有数据属性占用的总内存量,单位为字节。 sizeof() 可能对某些属性进行填充,使其与字边界对齐,也可能不这样做,这取决于您使用的编译器。

友元类和友元函数

可以访问类的私有private的属性和方法

#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
   friend void DisplayAge(const Human& person);
   string name;
   int age;

public:
   Human(string humansName, int humansAge) 
   {
      name = humansName;
      age = humansAge;
   }
};

void DisplayAge(const Human& person)
{
   cout << person.age << endl;
}

int main()
{
   Human firstMan("Adam", 25);
   cout << "Accessing private member age via friend function: ";
   DisplayAge(firstMan);

   return 0;
}#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
   friend class Utility;
   string name;
   int age;

public:
   Human(string humansName, int humansAge) 
   {
      name = humansName;
      age = humansAge;
   }
};

class Utility
{
public:
   static void DisplayAge(const Human& person)
   {
      cout << person.age << endl;
   }
};

int main()
{
   Human firstMan("Adam", 25);
   cout << "Accessing private member age via friend class: ";
   Utility::DisplayAge(firstMan);

   return 0;
}

盲猜用于共同实现某一功能的两个模块,比如混动车的燃油发动机给电动机供电。

struct结构体,和类类似,属性默认为公开

关键字 struct 来自 C 语言,在 C++编译器看来,它与类及其相似,差别在于程序员未指定时,默认的访问限定符(public 和 private)不同。因此,除非指定了,否则结构中的成员默认为公有的(而类成员默认为私有的);另外,除非指定了,否则结构以公有方式继承基结构(而类为私有继承)。

union共用体

共用体是一种特殊的类,每次只有一个非静态数据成员处于活动状态。因此,共用体与类一样,可包含多个数据成员,但不同的是只能使用其中的一个。

与结构类似,共用体的成员默认也是公有的,但不同的是,共用体不能继承。另外,将 sizeof() 用于共用体时,结果总是为共用体最大成员的长度,即便该成员并不处于活动状态。

常见使用场景

#include <iostream>
using namespace std;

union SimpleUnion
{
   int num;
   char alphabet;
};

struct ComplexType
{
   enum DataType
   {
      Int,
      Char
   } Type;

   union Value
   {
      int num;
      char alphabet;

      Value() {}
      ~Value() {}
   }value;
};

void DisplayComplexType(const ComplexType& obj)
{
   switch (obj.Type)
   {
   case ComplexType::Int:
      cout << "Union contains number: " << obj.value.num << endl;
      break;

   case ComplexType::Char:
      cout << "Union contains character: " << obj.value.alphabet << endl;
      break;
   }
}

int main()
{
   SimpleUnion u1, u2;
   u1.num = 2100;
   u2.alphabet = 'C';

   // Alternative using aggregate initialization:
   // SimpleUnion u1{ 2100 }, u2{ 'C' }; // Note that 'C' still initializes first / int member

   cout << "sizeof(u1) containing integer: " << sizeof(u1) << endl;
   cout << "sizeof(u2) containing character: " << sizeof(u2) << endl;

   ComplexType myData1, myData2;
   myData1.Type = ComplexType::Int;
   myData1.value.num = 2017;

   myData2.Type = ComplexType::Char;
   myData2.value.alphabet = 'X';

   DisplayComplexType(myData1);
   DisplayComplexType(myData2);

   return 0;
}

/**
sizeof(u1) containing integer: 4
sizeof(u2) containing character: 4
Union contains number: 2017
Union contains character: X
*/

换句话说,这个结构使用枚举来存储信息类型,并使用共用体来存储实际值。这是共用体的一种常见用法,例如,在 Windows 应用程序编程中常用的结构 VARIANT 就以这样的方式使用了共用体.

聚合初始化

#include <iostream>
#include<string>
using namespace std;

class Aggregate1
{
public:
   int num;
   double pi;
};

struct Aggregate2
{
   char hello[6];
   int impYears[3];
   string world;
};

int main()
{
   int myNums[] = { 9, 5, -1 }; // myNums is int[3]
   Aggregate1 a1{ 2017, 3.14 };
   cout << "Pi is approximately: " << a1.pi << endl;

   Aggregate2 a2{ {'h', 'e', 'l', 'l', 'o'}, {2011, 2014, 2017}, "world"};

   // Alternatively
   Aggregate2 a2_2{'h', 'e', 'l', 'l', 'o', '\0', 2011, 2014, 2017, "world"};

   cout << a2.hello << ' ' << a2.world << endl;
   cout << "C++ standard update scheduled in: " << a2.impYears[2] << endl;

   return 0;
}

constexpr还可以用于类的构造函数和成员函数,编译器会尽可能将其视为常量处理

#include <iostream>
using namespace std;

class Human
{
    int age;
public:
    constexpr Human(int humansAge) :age(humansAge) {}
    constexpr int GetAge() const { return age; }
};

int main()
{
    constexpr Human somePerson(15);
    const int hisAge = somePerson.GetAge();

    Human anotherPerson(45); // not constant expression

    return 0;
}

最简单的继承

#include <iostream>
using namespace std; 


class Fish
{
public:
   bool isFreshWaterFish;

   void Swim()
   {
      if (isFreshWaterFish)
         cout << "Swims in lake" << endl;
      else
         cout << "Swims in sea" << endl;
   }
};

class Tuna: public Fish
{
public:
   Tuna()
   {
      isFreshWaterFish = false;
   }
};

class Carp: public Fish
{
public:
   Carp()
   {
      isFreshWaterFish = true;
   }
};

int main()
{
   Carp myLunch;
   Tuna myDinner;

   cout << "Getting my food to swim" << endl;

   cout << "Lunch: ";
   myLunch.Swim();

   cout << "Dinner: ";
   myDinner.Swim();

   return 0;
}

基类使用protected关键字,该成员只有子类和友元中可以访问,外部不可以访问

基类构造器可以带参数,子类构造器必须一起初始化基类的构造器

#include <iostream>
using namespace std; 

class Fish
{
protected:
   bool isFreshWaterFish; // accessible only to derived classes

public:
   // Fish constructor
   Fish(bool isFreshWater) : isFreshWaterFish(isFreshWater){}

   void Swim()
   {
      if (isFreshWaterFish)
         cout << "Swims in lake" << endl;
      else
         cout << "Swims in sea" << endl;
   }
};

class Tuna: public Fish
{
public:
   Tuna(): Fish(false) {}
};

class Carp: public Fish
{
public:
   Carp(): Fish(true) {}
};

int main()
{
   Carp myLunch;
   Tuna myDinner;

   cout << "Getting my food to swim" << endl;

   cout << "Lunch: ";
   myLunch.Swim();

   cout << "Dinner: ";
   myDinner.Swim();

   return 0;
}

基类属性和方法的覆写

如果派生类实现了从基类继承的函数,且返回值和特征标相同,就相当于覆盖了基类的这个方法。

如果基类的方法是public的,外部可以通过域解析运算符::来调用基类方法。 myDinner.Fish::Swim(); // invokes Fish::Swim() using instance of Tuna在子类中,同样用上述方法来调用。

隐藏基类方法

基类中有同名的重载方法时,子类覆写其中一个,子类中会对外隐藏所有的同名方法。

#include <iostream>
using namespace std; 
   
class Fish
{
public:
   void Swim()
   {
       cout << "Fish swims... !" << endl;
   }

   void Swim(bool isFreshWaterFish)
   {
      if (isFreshWaterFish)
         cout << "Swims in lake" << endl;
      else
         cout << "Swims in sea" << endl;
   }
};

class Tuna: public Fish
{
public:
   void Swim(bool isFreshWaterFish)
   {
       Fish::Swim(isFreshWaterFish);
   }

   void Swim()
   {
      cout << "Tuna swims real fast" << endl;
   }
};

int main()
{
   Tuna myDinner;

   cout << "Getting my food to swim" << endl;
   
   myDinner.Swim(false);//failure: Tuna::Swim() hides Fish::Swim(bool)

   myDinner.Swim();

   return 0;
}

要想解除隐藏,外部可以通过域解析运算符直接调用到基类的方法。或者在子类中使用using解除对基类方法的隐藏。

class Tuna: public Fish
{
public:
 using Fish::Swim; // unhide all Swim() methods in class Fish
 void Swim()
 {
 cout << "Tuna swims real fast" << endl;
 }
}; 

构造和析构顺序

构造时先构造基类部分,再构造子类部分。

析构时先调用子类的,再调用基类的。

class Car:private Motor私有继承

只有子类可以访问基类中的属性,方法,外部不可以调用基类方法。也不可以通过域解析运算符访问。

私有继承时,子类的子类同样不可以访问基类的方法。例如RaceCar继承自Car,它也访问不了Motor

protected保护继承

同样屏蔽了外部访问,但是使用保护继承的子类的子类可以访问到基类的属性,方法。

仅当必要时才使用私有或保护继承。

对于大多数使用私有继承的情形(如 Car 和 Motor 之间的私有继承),更好的选择是,将基类对象作为派生类的一个成员属性。通过继承 Motor 类,相当于对 Car 类进行了限制,使其只能有一台发动机,同时,相比于将 Motor 对象作为私有成员,没有任何好处可言。汽车在不断发展,例如,混合动力车除电力发动机外,还有一台汽油发动机。在这种情况下,让 Car 类继承 Motor 类将成为兼容性瓶颈。

class Car
{
private:
 Motor heartOfCar;
public:
 void Move()
 {
 heartOfCar.SwitchIgnition();
 heartOfCar.PumpFuel();
 heartOfCar.FireCylinders();
 }
}; 

切除问题

一个方法接受一个基类参数,但是传递一个子类对象过去,这时候,复制机制只会复制基类部分,子类的部分将被切除。

可以同时继承多个基类

使用final关键字禁止继承,表示其为最终的子类

多态

上面的切除问题可以使用多态的特性来规避。让我们可以用类似的方式处理不同类型的对象。

将基类的方法声明为虚函数,可以确保编译器调用子类中的覆写方法。

下面的例子中,子类中的方法声明为:virtual void Swim()

#include <iostream>
using namespace std;

class Fish
{
public:
   virtual void Swim()
   {
      cout << "Fish swims!" << endl;
   }
};

class Tuna:public Fish
{
public:
   // override Fish::Swim
   void Swim()
   {
      cout << "Tuna swims!" << endl;
   }
};

class Carp:public Fish
{
public:
   // override Fish::Swim
   void Swim()
   {
      cout << "Carp swims!" << endl;
   }
};

void MakeFishSwim(Fish& InputFish)
{
   // calling Swim
   InputFish.Swim();
}

int main() 
{
   Tuna myDinner;
   Carp myLunch;

   // sending Tuna as Fish
   MakeFishSwim(myDinner);

   // sending Carp as Fish
   MakeFishSwim(myLunch);

   return 0;
}

/**
Tuna swims!
Carp swims!
*/

因为存在覆盖版本 Tuna::Swim()Carp::Swim() ,它们优先于被声明为虚函数的 Fish::Swim() 。这很重要,它意味着在 MakeFishSwim() 中,可通过 Fish& 参数调用派生类定义的 Swim() ,而无需知道该参数指向的是哪种类型的对象。

这就是多态:将派生类对象视为基类对象,并执行派生类的 Swim() 实现。

类似的,析构函数也需要声明为虚析构函数

将子类指针当作基类指针传递参数时,函数体内调用删除方法,删除时只会调用基类的析构函数,子类的部分将不会回收,将造成内存泄漏。

将基类的析构函数声明为 vitual 的,再使用基类指针删除时,将确保调用到子类的析构函数。

#include <iostream>
using namespace std;

class Fish
{
public:
   Fish()
   {
      cout << "Constructed Fish" << endl;
   }
   virtual ~Fish()   // virtual destructor!
   {
      cout << "Destroyed Fish" << endl;
   }
};

class Tuna:public Fish
{
public:
   Tuna()
   {
      cout << "Constructed Tuna" << endl;
   }
   ~Tuna()
   {
      cout << "Destroyed Tuna" << endl;
   }
};

void DeleteFishMemory(Fish* pFish)
{
   delete pFish;
}

int main() 
{
   cout << "Allocating a Tuna on the free store:" << endl;
   Tuna* pTuna = new Tuna;
   cout << "Deleting the Tuna: " << endl;
   DeleteFishMemory(pTuna);

   cout << "Instantiating a Tuna on the stack:" << endl;
   Tuna myDinner;
   cout << "Automatic destruction as it goes out of scope: " << endl;

   return 0;
}

虚函数表

编译器将为实现了虚函数的基类和覆盖了虚函数的派生类分别创建一个虚函数表(VirtualFunction Table,VFT)。换句话说,Base 和 Derived 类都将有自己的虚函数表。实例化这些类的对象时,将创建一个隐藏的指针(我们称之为 VFT*),它指向相应的 VFT。可将 VFT 视为一个包含函数指针的静态数组,其中每个指针都指向相应的虚函数。

子类覆写了基类的某些虚函数时,子类的虚函数表的函数指针将指向子类自己的函数实现。

对于未覆写的基类函数,虚函数表中的函数指针会指向基类的函数。

外部调用时,就通过虚函数表来查找到底该调用的子类方法还是基类方法。

有纯虚函数的类可以称为抽象基类

纯虚函数定义:virtual void Swim() = 0;

子类继承了抽象基类,则必须覆写其定义的纯虚函数。

#include <iostream>
using namespace std;

class Fish
{
public:
   // define a pure virtual function Swim
   virtual void Swim() = 0;
};

class Tuna:public Fish
{
public:
   void Swim()
   {
      cout << "Tuna swims fast in the sea!" << endl;
   }
};

class Carp:public Fish
{
   void Swim()
   {
      cout << "Carp swims slow in the lake!" << endl;
   }
};

void MakeFishSwim(Fish& inputFish)
{
   inputFish.Swim();
}

int main()
{
   // Fish myFish;   // Fails, cannot instantiate an ABC
   Carp myLunch;
   Tuna myDinner;

   MakeFishSwim(myLunch);
   MakeFishSwim(myDinner);

   return 0;
}

虚继承解决菱形问题

鸭嘴兽具备哺乳动物、鸟类和爬行动物的特征,这意味着 Platypus 类需要继承Mammal、Bird 和 Reptile。然而,这些类都从同一个类—Animal 派生而来。全部使用常规继承方式,将创建三个animal的实例,甚至可以分别设置每一个的age属性。

当派生类可能用作基类时,使用vitual虚继承是更好的选择。这样当一个类继承多个从相同基类衍生而来的类时,只创建一个基类实例。

#include <iostream>
using namespace std;

class Animal
{
public:
   Animal()
   {
      cout << "Animal constructor" << endl;
   }

   // sample member
   int age;
};

class Mammal:public virtual Animal
{
};

class Bird:public virtual Animal
{
};

class Reptile:public virtual Animal
{
};

class Platypus final:public Mammal, public Bird, public Reptile
{
public:
   Platypus()
   {
      cout << "Platypus constructor" << endl;
   }
};

int main()
{
   Platypus duckBilledP;

   // no compile error as there is only one Animal::age
   duckBilledP.age = 25; 

   return 0;
}

/**
Animal constructor
Platypus constructor
*/

C++关键字 virtual 的含义随上下文而异(我想这样做的目的很可能是为了省事),对其含义总结如下:

  1. 在函数声明中,virtual 意味着当基类指针指向派生对象时,通过它可调用派生类的相应函数。
  2. 从 Base 类派生出 Derived1 和 Derived2 类时,如果使用了关键字 virtual,则意味着再从Derived1 和 Derived2 派生出 Derived3 时,每个 Derived3 实例只包含一个 Base 实例。

也就是说,关键字 virtual 被用于实现两个不同的概念。

override关键字

子类中覆写基类方法时,通过override关键字,检查基类中对应的方法是否声明了虚函数,防止标记错误导致覆写失败。好的编程习惯是每个子类覆写函数后都加入override标记。

虚复制构造函数

不可能实现虚复制构造函数,因为在基类方法声明中使用关键字 virtual 时,表示它将被派生类的实现覆盖,这种多态行为是在运行阶段实现的。而构造函数只能创建固定类型的对象,不具备多态性,因此 C++不允许使用虚复制构造函数。

可以通过自己基类定义虚函数,并在子类实现,一个专门的Clone函数,外部来显式调用,定义返回一个基类指针类型,传入子类指针。就可以让返回的基类指针在调用方法时表现为子类的特性。

#include <iostream>
using namespace std;

class Fish
{
public:
   virtual Fish* Clone() = 0;
   virtual void Swim() = 0;
   virtual ~Fish() {};
};

class Tuna: public Fish
{
public:
   Fish* Clone() override
   {
      return new Tuna (*this);
   }

   void Swim() override final
   {
      cout << "Tuna swims fast in the sea" << endl;
   }
};

class BluefinTuna final:public Tuna
{
public:
   Fish* Clone() override
   {
      return new BluefinTuna(*this);
   }

   // Cannot override Tuna::Swim as it is "final" in Tuna
};

class Carp final: public Fish
{
   Fish* Clone() override
   {
      return new Carp(*this);
   }
   void Swim() override final
   {
      cout << "Carp swims slow in the lake" << endl;
   }
};

int main()
{
   const int ARRAY_SIZE = 4;

   Fish* myFishes[ARRAY_SIZE] = {NULL};
   myFishes[0] = new Tuna();
   myFishes[1] = new Carp();
   myFishes[2] = new BluefinTuna();
   myFishes[3] = new Carp();

   Fish* myNewFishes[ARRAY_SIZE];
   for (int index = 0; index < ARRAY_SIZE; ++index)
      myNewFishes[index] = myFishes[index]->Clone();

   // invoke a virtual method to check
   for (int index = 0; index < ARRAY_SIZE; ++index)
      myNewFishes[index]->Swim();

   // memory cleanup
   for (int index = 0; index < ARRAY_SIZE; ++index)
   {
      delete myFishes[index];
      delete myNewFishes[index];
   }

   return 0;
}

对类使用单目运算符

在类中定义:

return_type operator operator_symbol (...parameter list...);

比如将Date类实现++运算操作。

// also contains postfix increment and decrement

#include <iostream>
using namespace std;

class Date
{
private:
   int day, month, year;

public:
   Date (int inMonth, int inDay, int inYear)
        : month (inMonth), day(inDay), year (inYear) {};

   Date& operator ++ () // prefix increment
   {
      ++day;
      return *this;
   }

   Date& operator -- () // prefix decrement
   {
      --day;
      return *this;
   }

   Date operator ++ (int) // postfix increment
   {
      Date copy(month, day, year);
      ++day;
      return copy;
   }

   Date operator -- (int) // postfix decrement
   {
      Date copy(month, day, year);
      --day;
      return copy;
   }

   void DisplayDate()
   {
      cout << month << " / " << day << " / " << year << endl;
   }
};

int main ()
{
   Date holiday (12, 25, 2016); // Dec 25, 2016

   cout << "The date object is initialized to: ";
   holiday.DisplayDate ();

   ++holiday; // move date ahead by a day
   cout << "Date after prefix-increment is: ";
   holiday.DisplayDate ();

   --holiday; // move date backwards by a day
   cout << "Date after a prefix-decrement is: ";
   holiday.DisplayDate ();

   return 0;
}

转换运算符

operator const char*()
{
 // operator implementation that returns a char*
}

在外部希望这个类以const char*的类型使用时,比如cout « date。可以类比java里的toString。

#include <iostream>
#include <sstream> // new include for ostringstream
#include <string>
using namespace std;

class Date
{
private:
   int day, month, year;
   string dateInString;

public:
   Date(int inMonth, int inDay, int inYear)
      : month(inMonth), day(inDay), year(inYear) {};

   operator const char*()
   {
      ostringstream formattedDate; // assists easy string construction
      formattedDate << month << " / " << day << " / " << year;
     
      dateInString = formattedDate.str();
      return dateInString.c_str();
   }
};
   
int main ()
{
   Date Holiday (12, 25, 2016);

   cout << "Holiday is on: " << Holiday << endl;

   // string strHoliday (Holiday); // OK!
   // strHoliday = Date(11, 11, 2016); // also OK!

   return 0;
}

智能指针初体验

#include <iostream>
#include <memory>  // new include to use unique_ptr
using namespace std;

class Date
{
private:
   int day, month, year;
   string dateInString;   

public:
   Date(int inMonth, int inDay, int inYear)
      : month(inMonth), day(inDay), year(inYear) {};

   void DisplayDate()
   {
      cout << month << " / " << day << " / " << year << endl;
   }
};

int main()
{
   unique_ptr<int> smartIntPtr(new int);
   *smartIntPtr = 42;

   // Use smart pointer type like an int*
   cout << "Integer value is: " << *smartIntPtr << endl;

   unique_ptr<Date> smartHoliday (new Date(12, 25, 2016));
   cout << "The new instance of date contains: ";

   // use smartHoliday just as you would a Date*
   smartHoliday->DisplayDate();

   return 0;
}

这个示例表明,可像使用普通指针那样使用智能指针,如第 23 和 32 行所示。第 23 行使用了smartIntPtr 来显示指向的 int 值,而第 32 行使用了 smartHoliday->DisplayData(),就像这两个变量的类型分别是 int和 Date。其中的秘诀在于,智能指针类 std::unique_ptr 实现了运算符和->

类实现双目加减法

#include <iostream>
using namespace std;
   
class Date
{
private:
   int day, month, year;
   string dateInString;

public:
   Date(int inMonth, int inDay, int inYear)
      : month(inMonth), day(inDay), year(inYear) {};
   
   Date operator + (int daysToAdd) // binary addition
   {
      Date newDate (month, day + daysToAdd, year);
      return newDate;
   }

   Date operator - (int daysToSub) // binary subtraction
   {
      return Date(month, day - daysToSub, year);
   }

   void DisplayDate()
   {
      cout << month << " / " << day << " / " << year << endl;
   }
};

int main()
{
   Date Holiday (12, 25, 2016);
   cout << "Holiday on: ";
   Holiday.DisplayDate ();

   Date PreviousHoliday (Holiday - 19);
   cout << "Previous holiday on: ";
   PreviousHoliday.DisplayDate();

   Date NextHoliday(Holiday + 6);
   cout << "Next holiday on: ";
   NextHoliday.DisplayDate ();

   return 0;
}

字符串使用+拼接

优化MyString类: 定义运算符+

MyString operator+ (const MyString& addThis)
{
 MyString newString;
 if (addThis.buffer != NULL)
 {
 newString.buffer = new char[GetLength() + strlen(addThis.buffer) + 1];
 strcpy(newString.buffer, buffer);
 strcat(newString.buffer, addThis.buffer);
 }
 return newString;
}

重载==和!=运算符

定义这两个运算符之前,编译器会直接比较二进制数据,简单对象可以正常返回正确结果。但是如果有char* 等指针数据,我们需要比较的是其指向的数据,而不是指针成员的地址值。

#include <iostream>
using namespace std;

class Date
{
private:
    int day, month, year;

public:
    Date(int inMonth, int inDay, int inYear)
        : month(inMonth), day(inDay), year(inYear) {}

   bool operator== (const Date& compareTo)
   {
      return ((day == compareTo.day) 
            && (month == compareTo.month) 
           && (year == compareTo.year));
   }

   bool operator!= (const Date& compareTo)
   {
      return !(this->operator==(compareTo));
   }

   void DisplayDate()
   {
       cout << month << " / " << day << " / " << year << endl;
   }
};
   
int main()
{
   Date holiday1 (12, 25, 2016);
   Date holiday2 (12, 31, 2016);

   cout << "holiday 1 is: ";
   holiday1.DisplayDate();
   cout << "holiday 2 is: ";
   holiday2.DisplayDate();

   if (holiday1 == holiday2)
      cout << "Equality operator: The two are on the same day" << endl;
   else
      cout << "Equality operator: The two are on different days" << endl;

   if (holiday1 != holiday2)
      cout << "Inequality operator: The two are on different days" << endl;
   else
      cout << "Inequality operator: The two are on the same day" << endl;

   return 0;
}

/**
holiday 1 is: 12 / 25 / 2016
holiday 2 is: 12 / 31 / 2016
Equality operator: The two are on different days
Inequality operator: The two are on different days 
*/

小于(<)、大于(>)、小于等于(<=)和大于等于(>=)运算符大致同上 覆写,并指定自己的一套比较标准,返回结果即可。

复制赋值运算符=

复制构造函数是通过复制场景来创建一个类时调用,这个是将一个类通过复制运算符赋给另一个类时使用。

需要清空当前类里需要覆盖的部分,使用新值。

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
   char* buffer;
   
public:
   MyString(const char* initialInput)
   {
      if(initialInput != NULL)
      {
         buffer = new char [strlen(initialInput) + 1];
         strcpy(buffer, initialInput);
     }
      else 
         buffer = NULL;
   }

   // Copy assignment operator
   MyString& operator= (const MyString& CopySource)
   {
      if ((this != &CopySource) && (CopySource.buffer != NULL))
      {
         if (buffer != NULL)
          delete[] buffer;

         // ensure deep copy by first allocating own buffer 
         buffer = new char [strlen(CopySource.buffer) + 1];

         // copy from the source into local buffer
         strcpy(buffer, CopySource.buffer);
      }

     return *this;
   }

   operator const char*()
   {
      return buffer;
   }

   ~MyString()
   {
       delete[] buffer;
   }

   MyString(const MyString& CopySource)
   {
       cout << "Copy constructor: copying from MyString" << endl;

       if (CopySource.buffer != NULL)
       {
           // ensure deep copy by first allocating own buffer 
           buffer = new char[strlen(CopySource.buffer) + 1];

           // copy from the source into local buffer
           strcpy(buffer, CopySource.buffer);
       }
       else
           buffer = NULL;
   }
};
   
int main()
{
   MyString string1("Hello ");
   MyString string2(" World");

   cout << "Before assignment: " << endl;
   cout << string1 << string2 << endl;
   string2 = string1;
   cout << "After assignment string2 = string1: " << endl;
   cout << string1 << string2 << endl;

   return 0;
}

/**
Before assignment:
Hello World
After assignment string2 = string1:
Hello Hello 
*/

如果您编写的类管理着动态分配的资源(如使用 new 分配的数组),除构造函数和析构函数外,请务必实现复制构造函数和复制赋值运算符。

如果没有解决对象被复制时出现的资源所有权问题,您的类就是不完整的,使用时甚至会影响应用程序的稳定性

下标运算符[]

编写封装了动态数组的类(如封装了 char* buffer 的 MyString)时,通过实现下标运算符,可轻松地随机访问缓冲区中的各个字符。

#include <iostream>
#include <string>
#include <string.h>
using namespace std;

class MyString
{
private:
   char* Buffer;
   
   // private default constructor
   MyString() {}

public:
   // constructor
   MyString(const char* InitialInput)
   {
      if(InitialInput != NULL)
      {
         Buffer = new char [strlen(InitialInput) + 1];
         strcpy(Buffer, InitialInput);
      }
      else 
         Buffer = NULL;
   }

   MyString operator + (const char* stringIn)
   {
      string strBuf(Buffer);
      strBuf += stringIn;
      MyString ret(strBuf.c_str());
      return ret;
   }

   // Copy constructor
   MyString(const MyString& CopySource)
   {
      if(CopySource.Buffer != NULL)
      {
         // ensure deep copy by first allocating own buffer 
         Buffer = new char [strlen(CopySource.Buffer) + 1];

         // copy from the source into local buffer
         strcpy(Buffer, CopySource.Buffer);
      }
      else 
         Buffer = NULL;
   }

   // Copy assignment operator
   MyString& operator= (const MyString& CopySource)
   {
      if ((this != &CopySource) && (CopySource.Buffer != NULL))
      {
         if (Buffer != NULL)
          delete[] Buffer;

         // ensure deep copy by first allocating own buffer 
         Buffer = new char [strlen(CopySource.Buffer) + 1];

         // copy from the source into local buffer
         strcpy(Buffer, CopySource.Buffer);
      }

     return *this;
   }

   const char& operator[] (int Index) const
   {
      if (Index < GetLength())
         return Buffer[Index];
   }
  
   // Destructor
   ~MyString()
   {
      if (Buffer != NULL)
         delete [] Buffer;
   }

   int GetLength() const
   {
      return strlen(Buffer);
   }

   operator const char*()
   {
      return Buffer;
   }
};

int main()
{
   cout << "Type a statement: ";
   string strInput;
   getline(cin, strInput);

   MyString youSaid(strInput.c_str());

   cout << "Using operator[] for displaying your input: " << endl;
   for (int index = 0; index < youSaid.GetLength(); ++index)
      cout << youSaid[index] << " ";
   cout << endl;

   cout << "Enter index 0 - " << youSaid.GetLength() - 1 << ": ";
   int index = 0;
   cin >> index;
   cout << "Input character at zero-based position: " << index;
   cout << " is: " << youSaid[index] << endl;

   return 0;
}

/**

Type a statement: Hey subscript operators[] are fabulous
Using operator[] for displaying your input:
H e y s u b s c r i p t o p e r a t o r s [ ] a r e f a b u l o u s
Enter index 0 - 37: 2
Input character at zero-based position: 2 is: y
*/

定义下标运算符时,如果不允许修改内部数组,则将返回的引用定义为const类型。将函数类型定义为const类型,禁止通过这个函数来修改其他的类成员属性。

如果需要通过这个函数去修改内部属性,则不定义成const。

函数运算符 operator()

operator() 让对象像函数,被称为函数运算符。函数运算符用于标准模板库(STL)中,通常是 STL算法中,其用途包括决策。根据使用的操作数数量,这样的函数对象通常称为单目谓词或双目谓词。

#include <iostream>
#include <string>
using namespace std;

class Display
{
public:
   void operator () (string input) const
   {
      cout << input << endl;
   }
};

int main ()
{
   Display displayFuncObj;

   // equivalent to displayFuncObj.operator () ("Display this string!");
   displayFuncObj ("Display this string!"); 

   return 0;
}

这个运算符也称为 operator()函数,而 Display 对象也称为函数对象或 functor。

移动构造函数

从上述代码可知,相比于常规赋值构造函数和复制赋值运算符的声明,移动构造函数和移动赋值运算符的不同之处在于,输入参数的类型为 Sample&&。另外,由于输入参数是要移动的源对象,因此不能使用 const 进行限定,因为它将被修改。返回类型没有变,因为它们分别是构造函数和赋值运算符的重载版本。 在需要创建临时右值时,遵循 C++的编译器将使用移动构造函数(而不是复制构造函数)和移动赋值运算符(而不是复制赋值运算符)。

移动构造函数和移动赋值运算符的实现中,只是将资源从源移到目的地,而没有进行复制。

在执行下面的拼接加赋值sayHelloAgain = Hello + World + CPP这个过程中,首先调用Hello的+运算符函数,将World的内容加进来,创造一个临时对象(Hello World),同理再把cpp加进来,创造另一个临时对象(Hello World of C++)。然后编译器会调用sayHelloAgain的移动构造函数,将这个临时对象的内容赋给sayHelloAgain,然后删除临时对象的buffer。这个过程只使用了一次移动构造函数。

完整示例:

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
   char* buffer;

   MyString(): buffer(NULL) // private default constructor
   {
      cout << "Default constructor called" << endl;
   }

public:
   MyString(const char* initialInput) // constructor
   {
      cout << "Constructor called for: " << initialInput << endl;
      if(initialInput != NULL)
      {
         buffer = new char [strlen(initialInput) + 1];
         strcpy(buffer, initialInput);
      }
      else
         buffer = NULL;
   }

   MyString(MyString&& moveSrc) // move constructor
   {
      cout << "Move constructor moves: " << moveSrc.buffer << endl;
      if(moveSrc.buffer != NULL)
      {
         buffer = moveSrc.buffer; // take ownership i.e.  'move'
         moveSrc.buffer = NULL;   // free move source
      }
    }

   MyString& operator= (MyString&& moveSrc) // move assignment op.
   {
      cout << "Move assignment op. moves: " << moveSrc.buffer << endl;
      if((moveSrc.buffer != NULL) && (this != &moveSrc))
      {
         delete[] buffer; // release own buffer

         buffer = moveSrc.buffer; // take ownership i.e.  'move'
         moveSrc.buffer = NULL;   // free move source
      }

      return *this;
   }

   MyString(const MyString& copySrc) // copy constructor
   {
      cout << "Copy constructor copies: " << copySrc.buffer << endl;
      if (copySrc.buffer != NULL)
      {
         buffer = new char[strlen(copySrc.buffer) + 1];
         strcpy(buffer, copySrc.buffer);
      }
      else
         buffer = NULL;
   }

   MyString& operator= (const MyString& copySrc) // Copy assignment op.
   {
      cout << "Copy assignment op. copies: " << copySrc.buffer << endl;
      if ((this != &copySrc) && (copySrc.buffer != NULL))
      {
         if (buffer != NULL)
            delete[] buffer;

         buffer = new char[strlen(copySrc.buffer) + 1];
         strcpy(buffer, copySrc.buffer);
      }

      return *this;
   }

   ~MyString() // destructor
   {
      if (buffer != NULL)
         delete[] buffer;
   }

   int GetLength()
   {
      return strlen(buffer);
   }

   operator const char*()
   {
      return buffer;
   }
   
   MyString operator+ (const MyString& addThis)
   {
      cout << "operator+ called: " << endl;
      MyString newStr;

      if (addThis.buffer != NULL)
      {
         newStr.buffer = new char[GetLength()+strlen(addThis.buffer)+1];
         strcpy(newStr.buffer, buffer);
         strcat(newStr.buffer, addThis.buffer);
      }

      return newStr;
   }
};

int main()
{
   MyString Hello("Hello ");
   MyString World("World");
   MyString CPP(" of C++");

   MyString sayHelloAgain ("overwrite this");
   sayHelloAgain = Hello + World + CPP;

   return 0;
}

/*
Without move constructor and move assignment operator:
Constructor called for: Hello
Constructor called for: World
Constructor called for:  of C++
Constructor called for: overwrite this
operator+ called:
Default constructor called
Copy constructor to copy from: Hello World
operator+ called:
Default constructor called
Copy constructor to copy from: Hello World of C++
Copy assignment operator to copy from: Hello World of C++

With move constructor and move assignment operators:
Constructor called for: Hello
Constructor called for: World
Constructor called for:  of C++
Constructor called for: overwrite this
operator+ called:
Default constructor called
Move constructor to move from: Hello World
operator+ called:
Default constructor called
Move constructor to move from: Hello World of C++
Move assignment operator to move from: Hello World of C++
*/

学习过程中复制构造函数的一个问题

当使用临时对象当作函数参数进行值传递时,将不会走复制构造函数,而是直接使用这个对象。

自定义字面量

涉及热力学的温度声明,采用如下方式:

Temperature k1 = 32.15_F;
Temperature k2 = 0.0_C; ReturnType operator "" YourLiteral(ValueType value)
{
 // conversion code here
} 

实例:

#include <iostream>
using namespace std;

struct Temperature
{
   double Kelvin;
   Temperature(long double kelvin) : Kelvin(kelvin) {}
};

Temperature operator"" _C(long double celcius)
{
   return Temperature(celcius + 273);
}

Temperature operator "" _F(long double fahrenheit)
{
   return Temperature((fahrenheit + 459.67) * 5 / 9);
}

int main()
{
   Temperature k1 = 31.73_F;
   Temperature k2 = 0.0_C;

   cout << "k1 is " << k1.Kelvin << " Kelvin" << endl;
   cout << "k2 is " << k2.Kelvin << " Kelvin" << endl;

   return 0;
}

/**
k1 is 273 Kelvin
k2 is 273 Kelvin 
*/

static_cast类型转换

static_cast 用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换—这种转换原本将自动或隐式地进行。

将 Derived转换为 Base被称为向上转换,无需使用任何显式类型转换运算符就能进行这种转换:

Derived objDerived;
Base* objBase = &objDerived; // ok!

将 Base转换为 Derived被称为向下转换,如果不使用显式类型转换运算符,就无法进行这种转换:

Derived objDerived;
Base* objBase = &objDerived; // Upcast -> ok!
Derived* objDer = objBase; // Error: Downcast needs explicit cast

可以利用static_cast进行向下转换,而不会报错。

Base* objBase = new Base();
Derived* objDer = static_cast<Derived*>(objBase); // Still no errors!

然而,static_cast 只验证指针类型是否相关,而不会执行任何运行阶段检查。

因此 objDer ->DerivedFunction() 能够通过编译,但在运行阶段可能导致意外结果。

dynamic_cast隐式转换

给定一个指向基类对象的指针,程序员可使用 dynamic_cast 进行类型转换,并在使用指针前检查指针指向的目标对象的类型。

Base* objBase = new Derived();
// Perform a downcast
Derived* objDer = dynamic_cast<Derived*>(objBase);
if(objDer) // Check for success of the cast
objDer->CallDerivedFunction (); 

这种在运行阶段识别对象类型的机制称为

运行阶段类型识别(runtime type identification,RTTI)。

#include <iostream>
using namespace std; 
  
class Fish
{
public:
   virtual void Swim()
   {
      cout << "Fish swims in water" << endl;
   }

   // base class should always have virtual destructor
   virtual ~Fish() {}   
};

class Tuna: public Fish
{
public:
   void Swim()
   {
      cout << "Tuna swims real fast in the sea" << endl;
   }

   void BecomeDinner()
   {
      cout << "Tuna became dinner in Sushi" << endl;
   }
};

class Carp: public Fish
{
public:
   void Swim()
   {
      cout << "Carp swims real slow in the lake" << endl;
   }

   void Talk()
   {
      cout << "Carp talked carp!" << endl;
   }
};

void DetectFishType(Fish* objFish)
{
   Tuna* objTuna = dynamic_cast <Tuna*>(objFish);
   if (objTuna)
   {
      cout << "Detected Tuna. Making Tuna dinner: " << endl;
      objTuna->BecomeDinner();   // calling Tuna::BecomeDinner
   }

   Carp* objCarp = dynamic_cast <Carp*>(objFish);
   if(objCarp)
   {
      cout << "Detected Carp. Making carp talk: " << endl;
      objCarp->Talk();  // calling Carp::Talk
   }

   cout << "Verifying type using virtual Fish::Swim: " << endl;
   objFish->Swim(); // calling virtual function Swim
}

int main()
{
   Carp myLunch;
   Tuna myDinner;

   DetectFishType(&myDinner);
   DetectFishType(&myLunch);

   return 0;
}

务必检查 dynamic_cast 的返回值,看它是否有效。如果返回值为 NULL,说明转换失败。

reinterpret_cast强制转换

可以做任何类型转换。

这种类型转换实际上是强制编译器接受 static_cast 通常不允许的类型转换,通常用于低级程序(如驱动程序),在这种程序中,需要将数据转换为 API(应用程序编程接口)能够接受的简单类型(例如,有些 OS 级 API 要求提供的数据为 BYTE 数组,即 unsigned char*)。

由于其他 C++类型转换运算符都不允许执行这种有悖类型安全的转换,因此除非万不得已,否则不要使用 reinterpret_cast 来执行不安全(不可移植)的转换。

const_cast将const类型转换成非const

某些情况,使用的类我们无法修改,其内部如果使用了不合理的非const函数,外部的const对象指针无法使用非const函数,就可以将外部的const对象转换成非const,实现调用。

void DisplayAllData (const SomeClass* data)
{
 // data->DisplayMembers(); Error: attempt to invoke a non-const function!
 SomeClass* pCastedData = const_cast<SomeClass*>(data);
 pCastedData->DisplayMembers(); // Allowed!
}

使用类型转换需要注意

在现代 C++中,除 dynamic_cast 外的类型转换都是可以避免的。仅当需要满足遗留应用程序的需求时,才需要使用其他类型转换运算符。在这种情况下,程序员通常倾向于使用 C 风格类型转换而不是C++类型转换运算符。重要的是,应尽量避免使用类型转换;而一旦使用类型转换,务必要知道幕后发生的情况。

预处理

顾名思义,预处理器在编译器之前运行,换句话说,预处理器根据程序员的指示,决定实际要编译的内容。预处理器编译指令都以#打头。

宏定义常量

#include <iostream>
#include<string>
using namespace std;

#define ARRAY_LENGTH 25
#define PI 3.1416
#define MY_DOUBLE double
#define FAV_WHISKY "Jack Daniels"

/*
// Superior alternatives (comment those above when you uncomment these)
const int ARRAY_LENGTH = 25;
const double PI = 3.1416;
typedef double MY_DOUBLE;
const char* FAV_WHISKY = "Jack Daniels";
*/

int main()
{
   int MyNumbers [ARRAY_LENGTH] = {0};
   cout << "Array's length: " << sizeof(MyNumbers) / sizeof(int) << endl;

   cout << "Enter a radius: ";
   MY_DOUBLE Radius = 0;
   cin >> Radius;
   cout << "Area is: " << PI * Radius * Radius << endl;

   string FavoriteWhisky (FAV_WHISKY);
   cout << "My favorite drink is: " << FAV_WHISKY << endl;

   return 0;
}

定义常量时,更好的选择是使用关键字 const 和数据类型,因此下面的定义好得多:

const int ARRAY_LENGTH = 25;
const double PI = 3.1416;
const char* FAV_WHISKY = "Jack Daniels";
typedef double MY_DOUBLE; // typedef aliases a type

最常用的宏功能

如果在头文件 class1.h 中声明了一个类,而这个类将 class2.h 中声明的类作为其成员,则需要在 class1.h 中包含 class2.h。如果设计非常复杂,即第二个类需要第一个类,则在 class2.h 中也需要包含 class1.h!然而,在预处理器看来,两个头文件彼此包含对方会导致递归问题。

为了防止循环引用,可以使用#ifndef 强制这种引用只执行一次:

#ifndef HEADER1_H _//multiple inclusion guard:
#define HEADER1_H_ // preprocessor will read this and following lines once
#include <header2.h>
class Class1{ 
// class members
};
#endif // end of header1.h

header2.h 与此类似,但宏定义不同,且包含的是<header1.h>:

#ifndef HEADER2_H_  //multiple inclusion guard
#define HEADER2_H_
#include<header1.h>
class Class2{ 
// class members
};
#endif // end of header2.h 

#ifndef 可读作 if-not-defined。这是一个条件处理命令,让预处理器仅在标识符未定义时才继续。#endif 告诉预处理器,条件处理指令到此结束。

因此,预处理器首次处理 header1.h 并遇到#ifndef 后,发现宏 HEADER1H_还未定义,因此继续处理。#ifndef 后面的第一行定义了宏 HEADER1_H,确保预处理器再次处理该文件时,将在遇到包含#ifndef 的第一行时结束,因为其中的条件为 false.

#define定义宏函数

例如平方计算,#define SQUARE(x) ((x) * (x)) 相比于常规函数调用,宏函数的优点在于,它们将在编译前就地展开,因此在有些情况下有助于改善代码的性能。而且一个宏可使用另一个宏。比如计算面积的宏函数可以使用定义的宏变量PI。 有个缺点是宏函数不考虑数据类型,返回值精度依赖输入的精度。

为什么要加这么多括号?

因为宏是最简单的替换,不会提前计算。如果去掉括号: #define AREA_CIRCLE(r) (PIrr) 如果使用类似于下面的语句调用这个宏,结果将如何呢? cout « AREA_CIRCLE (4+6); 展开后,编译器看到的语句如下: cout « (PI4+64+6); // not the same as PI1010 根据运算符优先级,将先执行乘法运算,再执行加法运算,因此编译器将这样计算面积: cout « (PI*4+24+6); // 42.5664 (which is incorrect) 在省略了括号的情况下,简单的文本替换破坏了编程逻辑!

assert宏

可以插入到某些地方来验证执行结果。需要提前包含<assert.h>

assert (expression that evaluates to true or false); 

如果条件不满足,它将抛出一个错误信息。

由于断言在发布模式下不可用,对于对应用程序正确运行至关重要的检查(如检查dynamic_cast 的返回值),为了确保它们在发布模式下也会执行,应使用 if 语句,这很重要。断言可帮助您找出问题,但不能因此不在代码中对指针做必要的检查。

使用宏的应该和不应该事项

  1. 尽可能不要自己编写宏函数。
  2. 尽可能使用 const 变量,而不是宏常量。
  3. 请牢记,宏并非类型安全的,预处理器不执行类型检查。
  4. 在宏函数的定义中,别忘了使用括号将每个变量括起。
  5. 为了在头文件中避免多次包含,别忘了使用#ifndef、#define 和#endif。
  6. 别忘了在代码中大量使用 assert( ),它们在发行版本中将被禁用,但对提高代码的质量很有帮助。

模板

模板让程序员能够定义一种适用于不同类型对象的行为。这听起来有点像宏(参见前面用于判断两个数中哪个更大的简单宏 MAX),但宏不是类型安全的,而模板是类型安全的。

编写一个比较大小的模板函数,它的接收的类型可以根据传参的类型自动生成多个重载函数。

#include<iostream>
#include<string>
using namespace std;

template <typename Type>
const Type& GetMax (const Type& value1, const Type& value2)
{
    if (value1 > value2)
        return value1;
    else
        return value2;
}

template <typename Type>
void DisplayComparison(const Type& value1, const Type& value2)
{
   cout << "GetMax(" << value1 << ", " << value2 << ") = ";
   cout << GetMax(value1, value2) << endl;
}

int main()
{
   int num1 = -101, num2 = 2011;
   DisplayComparison<int>(num1, num2);

   double d1 = 3.14, d2 = 3.1416;
   DisplayComparison(d1, d2);

   string name1("Jack"), name2("John");
   DisplayComparison(name1, name2);

   return 0;
}

上述代码将导致编译器生成模板函数 GetMax 的两个版本。 如果进行调用的参数不一致,比如传一个int和srting类型一起比较大小,将导致编译错误。

模板类

类是设计对象的蓝图,而模板类就是蓝图的蓝图。

可以在类里面设置模板参数,让同一个类的同一个字段使用不同的类型来表示。

template <typename T1, typename T2>
class HoldsPair
{
private:
 T1 value1;
 T2 value2;
public:
 // Constructor that initializes member variables
 HoldsPair (const T1& val1, const T2& val2)
 {
 value1 = val1;
 value2 = val2;
 };
 // ... Other member functions
};

// 在这里,类 HoldsPair 接受两个模板参数,参数名分别为 T1 和 T2。可使用这个类来存储两个类型
// 相同或不同的对象,如下所示:
// A template instantiation that pairs an int with a double
HoldsPair <int, double> pairIntDouble (6, 1.99);
// A template instantiation that pairs an int with an int
HoldsPair <int, int> pairIntDouble (6, 500); 

还可以设置默认类型简化使用,如果实例使用的和默认类型相同,则可以简化对象的声明方式。

// template with default params: int & double
template <typename T1=int, typename T2=double>
class HoldsPair
{
private:
   T1 value1;
   T2 value2;
public:
   HoldsPair(const T1& val1, const T2& val2) // constructor
      : value1(val1), value2(val2) {}
 
   // Accessor functions
   const T1 & GetFirstValue () const 
   {
      return value1;
   }

   const T2& GetSecondValue () const
   {
      return value2;
   }
};
   
#include <iostream>
using namespace std;

int main ()
{
   HoldsPair <> pairIntDbl (300, 10.09);
   HoldsPair <short, const char*> pairShortStr(25, "Learn templates, love C++");

   cout << "The first object contains -" << endl;
   cout << "Value 1: " << pairIntDbl.GetFirstValue () <<  endl;
   cout << "Value 2: " << pairIntDbl.GetSecondValue () << endl;

   cout << "The second object contains -" << endl; 
   cout << "Value 1: " << pairShortStr.GetFirstValue () <<  endl;
   cout << "Value 2: " << pairShortStr.GetSecondValue () << endl;

   return 0;
}

模板实例化和具体化

定义但不使用的模板,编译器将忽略。因此,对模板来说,实例化指的是使用一个或多个模板参数来创建特定的类型。

HoldsPair<int, double> pairIntDbl;

相当于编译器使用这个模板创建了一个类。

另一方面,在有些情况下,使用特定的类型实例化模板时,需要显式地指定不同的行为。这就是具体化模板,即为特定的类型指定行为。

#include <iostream>
using namespace std;

template <typename T1 = int, typename T2 = double>
class HoldsPair
{
private:
   T1 value1;
   T2 value2;
public:
   HoldsPair(const T1& val1, const T2& val2) // constructor
      : value1(val1), value2(val2) {}

   // Accessor functions
   const T1 & GetFirstValue() const;
   const T2& GetSecondValue() const;
};

// specialization of HoldsPair for types int & int here
template<> class HoldsPair<int, int>
{
private:
   int value1;
   int value2;
   string strFun;
public:
   HoldsPair(const int& val1, const int& val2) // constructor
      : value1(val1), value2(val2) {}

   const int & GetFirstValue() const
   {
      cout << "Returning integer " << value1 << endl;
      return value1;
   }
};
   
int main()
{
   HoldsPair<int, int> pairIntInt(222, 333);
   pairIntInt.GetFirstValue();

   return 0;
}

带静态变量的模板类

#include <iostream>
using namespace std;

template <typename T>
class TestStatic
{
public:
   static int staticVal;
};
   
// static member initialization
template<typename T> int TestStatic<T>::staticVal;

int main()
{
   TestStatic<int> intInstance;
   cout << "Setting staticVal for intInstance to 2011" << endl;
   intInstance.staticVal = 2011;

   TestStatic<double> dblnstance;
   cout << "Setting staticVal for Double_2 to 1011" << endl;
   dblnstance.staticVal = 1011;

   cout << "intInstance.staticVal = " << intInstance.staticVal << endl;
   cout << "dblnstance.staticVal = " << dblnstance.staticVal << endl;

   return 0;
}

/**
Setting staticVal for intInstance to 2011
Setting staticVal for Double_2 to 1011
intInstance.staticVal = 2011
dblnstance.staticVal = 1011
*/

也就是说,如果模板类包含静态成员,该成员将在针对 int 具体化的所有实例之间共享;同样,它还将在针对 double 具体化的所有实例之间共享,且与针对 int 具体化的实例无关。换句话说,可以认为编译器创建了两个版本的 x:x_int 用于针对 int 具体化的实例,而 x_double 针对 double 具体化的实例。

参数数量可变的模板函数

参数数量可变的模板是 2014 年发布的 C++14 新增的。

#include <iostream>
using namespace std;

template <typename Res, typename ValType>
void Sum(Res& result, ValType& val)
{
   result = result + val;
}

template <typename Res, typename First, typename... Rest> 
void Sum(Res& result, First val1, Rest... numN)
{
   result = result + val1;
   return Sum(result, numN ...);
}

int main()
{
   double dResult = 0;
   Sum (dResult, 3.14, 4.56, 1.1111);
   cout << "dResult = " << dResult << endl;

   string strResult;
   Sum (strResult, "Hello ", "World");
   cout << "strResult = " << strResult.c_str() << endl;

   return 0;
}

您可能注意到了,在前面的代码示例中,使用了省略号…。在 C++中,模板中的省略号告诉编译器,默认类或模板函数可接受任意数量的模板参数,且这些参数可为任何类型。

元组

通过索引访问的一种数据结构,内部可以存储多种不同的数据类型。

#include <iostream>
#include <tuple>
#include <string>
using namespace std;

template <typename tupleType>
void DisplayTupleInfo(tupleType& tup)
{
   const int numMembers = tuple_size<tupleType>::value;
   cout << "Num elements in tuple: " << numMembers << endl;
   cout << "Last element value: " << get<numMembers - 1>(tup) << endl;
}

int main()
{
   tuple<int, char, string> tup1(make_tuple(101, 's', "Hello Tuple!"));
   DisplayTupleInfo(tup1);

   auto tup2(make_tuple(3.14, false));
   DisplayTupleInfo(tup2);

   auto concatTup(tuple_cat(tup2, tup1)); // contains tup2, tup1 members
   DisplayTupleInfo(concatTup);

   double pi;
   string sentence;
   tie(pi, ignore, ignore, ignore, sentence) = concatTup;
   cout << "Unpacked! Pi: " << pi << " and \"" << sentence << "\"" << endl;

    return 0;
}

static_assert不满足条件直接禁止编译

static_assert 是 C++11 新增的一项功能,让您能够在不满足指定条件时禁止编译。这好像不可思议,但对模板类来说很有用。例如,您可能想禁止针对 int 实例化模板类,为此可使用 static_assert,它是一种编译阶段断言,可用于在开发环境(或控制台中)显示一条自定义消息。

template <typename T>
class EverythingButInt
{
public:
   EverythingButInt()
   {
      static_assert(sizeof(T) != sizeof(int), "No int please!");
   }
};

int main()
{
   EverythingButInt<int> test;

   return 0;
}

待补充STL标准库

【跨平台】IOS端应用开发扫盲

【跨平台】IOS端应用开发扫盲

本文从初学者角度出发,介绍IOS端应用开发的一些基础知识。

最新在KMP的开发过程中,对于一些IOS端的全局性的UI样式代码修改,有些无从下手。Swift语言层面上还比较容易看懂,但是对于系统规则机制,app运行机制等了解尚浅,对此做一个基础的总结。

开发语言层面对比

1. 语言特性对比

特性SwiftJava异同点与优势
类型系统强类型,类型推断强类型,类型推断有限Swift 的类型推断更强大,很多时候无需显式声明变量类型,代码更简洁。
内存管理自动引用计数 (ARC)垃圾回收 (GC)这是两者最核心的区别。Swift 的 ARC 性能更高,但需要注意循环引用;Java 的 GC 开发者更省心,但可能带来运行时卡顿。
可选类型Optional 类型NullSwift 强制处理 nil,开发者必须显式地用 if letguard let 解包,从而从语言层面杜绝了空指针异常。Java 的 NullPointerException 是一个常见痛点。
编程范式面向对象、函数式纯面向对象Swift 融合了面向对象、函数式和协议导向编程(Protocol-Oriented Programming, POP)思想,代码更灵活,尤其在泛型和协议方面。
结构体支持结构体 (Struct)不支持,只有类Swift 的结构体是值类型,类是引用类型。这提供了更多的灵活性和性能优化空间,例如在处理轻量级数据时使用结构体可以避免不必要的内存分配和引用计数开销。
函数式支持高阶函数、闭包支持 lambda 表达式Swift 的闭包(Closures)功能强大且易用,是其函数式编程特性的重要体现。
多线程GCD、Operation QueueThread, Executor, Coroutines两者都提供了完善的多线程解决方案,但具体实现方式不同。Swift 的 GCD (Grand Central Dispatch) 是一个非常强大的基于任务队列的并发模型。

2. 运行环境对比

环境SwiftJava异同点与优势
运行时原生 (Native) 运行时虚拟机 (JVM / ART)Swift 代码直接编译成机器码在 CPU 上执行,没有虚拟机的性能开销,启动更快,执行效率更高。
编译过程LLVM 编译器Java 编译器(Javac)Swift 的 LLVM 编译器非常先进,能够生成高度优化的机器码。
跨平台主要用于 Apple 生态跨平台能力强大Swift 主要用于 iOS、macOS、watchOS 等苹果平台,虽然有开源项目尝试跨平台,但生态和工具链远不如 Java。Java 的 JVM 可以运行在 Windows、Linux、Android 等多个操作系统上,“一次编写,到处运行”
语言版本频繁更新稳定,但更新较慢Swift 语言发展迅速,版本更新频繁,新特性不断加入。Java 语言相对稳定,版本更新周期较长。

总的来说,Swift 直接编译成机器码,没有虚拟机开销,运行速度快。安全可靠,可选类型从语言层面消除了空指针异常,类型推断减少了编程错误。融合了多种编程范式,如函数式、协议导向等,语法简洁、富有表现力。

而 Java 得益于 JVM,Java 具有无可比拟的跨平台能力。其拥有庞大而成熟的社区和工具生态,有无数的框架和库可供选择。语言版本和 API 相对稳定,适合大型企业级应用开发。

代码编写到运行经历了哪些流程

1. 代码编写与编译阶段

在这个阶段,开发者用 Swift 或 Objective-C 语言编写代码。使用 Xcode 这个集成开发环境 (IDE),它包含了所有的工具链,如编译器、调试器等。

  • 编译器: Xcode 默认使用 LLVM (Low Level Virtual Machine) 编译器。
    • Swift 源码通过 Swift 编译器编译成 LLVM Intermediate Representation (IR),然后再编译成机器码。
    • Objective-C 源码则直接通过 Clang 编译器编译成机器码。
  • 编译优化: 编译器会对代码进行各种优化,例如 dead code elimination(移除无用代码)、常量折叠等,以提高应用的运行效率。
  • 产物: 编译的最终产物是可执行的机器码文件(Mach-O 文件),以及应用所需的其他资源文件(如图片、UI 布局文件等)。

2. 应用打包阶段

编译完成后,Xcode 会将所有必需的文件打包成一个可分发、可安装的格式。

  • Bundle 概念: iOS 应用的核心是一个 Bundle。它是一个特殊的文件夹,其目录结构是固定的。Bundle 内部包含了可执行文件、所有的资源文件(图片、声音、NIB/Storyboards 等)、以及一个重要的 Info.plist 文件。
  • Info.plist 文件: 这是一个属性列表文件,包含了应用的元数据,例如应用名称、版本号、支持的设备方向、所需的权限(如相机、位置服务)等等。操作系统和应用本身都会读取这个文件来获取关键信息。
  • IPA 文件: 最终,整个 Bundle 会被压缩成一个 .ipa 文件。.ipa 文件本质上是一个 ZIP 压缩包,.ipa 的作用就类似于 Android 的 .apk 文件。

3. 应用安装阶段

用户从 App Store 下载或通过其他方式获取到 .ipa 文件后,系统会进行安装。

  • 解压与签名验证: 系统首先解压 .ipa 文件,然后进行严格的数字签名验证。每一个在 App Store 上发布的 iOS 应用都必须由 Apple 签发证书进行签名。
    • 目的: 签名验证的目的是确保应用没有被篡改,且来自可信的开发者。这是 iOS 安全机制的重要一环。
  • 权限配置: 系统会根据 Info.plist 文件中声明的权限,为应用配置对应的沙盒环境。这决定了应用可以访问哪些系统资源和数据。
  • 目录结构: 应用的 Bundle 会被安装到 /private/var/containers/Bundle/Application 目录下。同时,系统还会为应用创建数据目录,包括 Documents、Library 和 tmp,这些目录位于 /private/var/mobile/Containers/Data/Application 下,用于应用存储数据。

4. 应用运行阶段

当用户点击应用图标时,应用开始启动和运行。

  • main() 函数: 所有 iOS 应用都从一个 main() 函数开始执行,这与 C/C++ 程序的入口点相同。
  • UIApplication: main() 函数会调用 UIApplicationMain 函数来创建一个 UIApplication 对象。UIApplication 是 iOS 应用的单例,负责管理应用的生命周期、事件循环和与系统之间的交互。
  • AppDelegate: UIApplication 会将应用的生命周期事件(如应用启动、进入后台、收到内存警告等)通知给 AppDelegate 对象。AppDelegate 是应用的代理,开发者可以在其中实现相应的回调方法,来处理这些系统事件。
  • 主线程: UI 更新、事件处理等所有与界面相关的操作都必须在 主线程 上执行。这与 Android 上的 UI 线程(Main thread)是相同的概念,都是为了避免并发问题,保证用户界面的流畅性。
  • 沙盒机制: 应用在运行过程中,其读写操作都严格限制在其沙盒目录内,无法访问沙盒外的其他应用数据,从而保证了系统安全和数据隔离。

APP运行环境

Android 应用运行在 ART(Android Runtime) 虚拟机上,这是一个基于 JIT(Just-In-Time)和 AOT(Ahead-Of-Time)编译的运行时环境,负责执行 Java/Kotlin 代码。它提供了垃圾回收、内存管理和沙箱隔离,确保每个应用都在一个独立、受保护的环境中运行。

iOS 应用则直接运行在 原生(Native) 环境下,执行由 Objective-CSwift 编写的代码。这些代码直接编译成机器码,由苹果的 Cocoa Touch 框架和 XNU 内核直接执行。

  • 没有虚拟机:iOS 不使用虚拟机,这使得其应用的启动速度和执行效率通常更高。
  • 内存管理:iOS 主要通过 ARC(Automatic Reference Counting) 机制来自动管理内存。当一个对象的引用计数变为零时,系统会自动回收它。这与 ART 的垃圾回收机制(Garbage Collection)不同,但目标都是为了简化内存管理。
  • 沙盒机制:和 Android 一样,iOS 也有严格的沙盒机制。每个应用都在一个独立的沙盒中运行,不能随意访问其他应用的数据。

内存管理

Android 和 iOS 在内存管理上采用了两种截然不同的策略,这直接影响了开发者编写代码的方式和对性能的考量。简单来说,Android 使用了垃圾回收 (Garbage Collection, GC),而 iOS 则依赖于自动引用计数 (Automatic Reference Counting, ARC)

1. Android 的内存管理:垃圾回收(GC)

安卓的垃圾回收是一种自动的内存管理机制。

  • 工作原理:
    • 在 Android 中,当一个对象不再被任何变量引用时,它就成了“垃圾”。
    • GC 线程会定期扫描内存中的所有对象。当它发现一个不再被引用的对象时,就会将其标记为可回收,并在合适的时机释放这块内存。
    • 开发者无需手动释放内存。
  • GC 的优缺点:
    • 优点: 开发者不需要关心何时释放内存,这大大降低了内存管理的复杂性,可以更专注于业务逻辑。
    • 缺点:
      • 不可控性: GC 的执行时机是不确定的。当 GC 运行时,它会暂停应用的主线程(这被称为 “Stop-The-World”),这可能导致应用的卡顿,尤其是在处理大量对象时。
      • 内存开销: GC 需要额外的内存来追踪和管理对象,这可能导致比 ARC 略高的内存占用。
  • 内存泄漏:
    • 尽管有 GC,但 Android 仍然会发生内存泄漏。最常见的情况是长生命周期对象引用了短生命周期对象
    • 例如,一个 Activity 对象被一个全局的单例对象引用,当 Activity 应该被销毁时,由于单例对象依然持有它的引用,GC 无法回收它,从而导致内存泄漏。开发者需要特别注意这种情况。

2. iOS 的内存管理:自动引用计数(ARC)

ARC 是一个编译器级别的内存管理机制。

  • 工作原理:
    • 每个对象都有一个引用计数器。当一个对象被创建时,其引用计数为 1。
    • 当一个变量引用了这个对象时,它的引用计数会加 1(称为 retain);当引用被移除时,引用计数会减 1(称为 release)。
    • 当引用计数减到 0 时,说明没有任何变量在使用这个对象,ARC 会立即释放这块内存。
    • 这个过程都是由编译器在编译时自动插入 retainrelease 代码来实现的,开发者无需手动调用。
  • ARC 的优缺点:
    • 优点:
      • 性能高: ARC 的内存管理在编译时完成,没有运行时暂停(Stop-The-World)的开销,因此性能更高,更适合对响应速度要求高的移动应用。
      • 可预测性: 内存释放的时机是确定的,一旦引用计数归零,对象就会立即被释放。
    • 缺点:
      • 循环引用(Retain Cycle): 这是 ARC 最主要的问题。当两个或多个对象相互持有对方的强引用时,它们的引用计数永远不会归零,导致内存无法被释放,造成内存泄漏。
  • 如何解决循环引用:
    • 开发者需要使用 弱引用(weak无主引用(unowned 来打破循环引用。
    • weak: 不会增加对象的引用计数。当引用的对象被释放后,weak 引用会自动置为 nil。常用于父子关系中,子对象对父对象的引用。
    • unowned: 类似于 weak,也不会增加引用计数,但它假定所引用的对象在它自身生命周期内永远不会被释放。如果引用的对象被释放了,访问 unowned 引用会引发运行时错误。常用于明确知道被引用对象生命周期比自身长的场景。

总的来说,Android 的 GC 让开发者更省心,但代价是可能牺牲部分性能。iOS 的 ARC 提供了更精细的控制和更高的性能,但要求开发者对引用关系有清晰的认识,并主动解决循环引用问题。理解这两种机制的优劣,是成为一名优秀的跨平台开发者必备的知识。

界面管理

Android 的 Activity 主要负责管理一个单独的用户界面(UI),并处理用户交互。它的生命周期(如 onCreateonPauseonDestroy)是开发者需要重点关注的。

iOS 中对应的概念是 UIViewController

  • UIViewController 是 iOS 应用界面的核心。每一个屏幕(或一个屏幕上的一个重要部分)都由一个 UIViewController 来管理。它负责管理视图(View),处理事件,并在用户界面和数据模型之间扮演桥梁角色。
  • 生命周期UIViewController 同样拥有自己的生命周期方法,例如 viewDidLoad(视图加载完成)、viewWillAppear(视图即将显示)和 viewWillDisappear(视图即将消失)。开发者需要在这几个方法中处理界面的初始化和状态保存。
  • 页面跳转:Android 通常通过 Intent 来启动新的 Activity。在 iOS 中,页面跳转通常通过 UINavigationController(用于堆栈式的页面管理)或 present(_:animated:completion:) 方法(用于模态弹出页面)来实现。

后台任务

Android 的 Service 主要用于在后台执行长时间运行的操作,且没有用户界面,比如下载文件或播放音乐。Service 可以在 Activity 被销毁后继续运行。

iOS 并没有一个与 Service 完全对应的组件,它的后台任务管理更加严格和精细化,主要通过以下几种机制实现:

  • 短暂的后台任务(Background Task):当应用从前台切换到后台时,系统会给它几秒钟的时间(通常是 3 到 10 分钟)来完成一些任务,例如保存数据或结束网络请求。
  • 后台模式(Background Modes):对于需要持续在后台运行的应用,如音乐播放器、位置追踪或 VoIP 电话,开发者需要在 Info.plist 文件中声明特定的“后台模式”。只有被系统明确允许的这些服务(如音乐播放、地理位置更新等)才能在后台持续运行。
  • 后台刷新(Background App Refresh):允许应用定期在后台刷新内容,但刷新时机由系统根据设备电量、网络状态等因素智能决定。
  • 静默推送(Silent Push Notifications):服务器可以向应用发送一种特殊的“静默推送”,唤醒应用在后台执行一小段代码(例如拉取新内容)。

【跨平台】Compose Desktop进行JNI开发

【跨平台】Compose Desktop进行JNI开发

本文介绍了使用Compose Multiplatform开发时,如何使用JNI接口

在 Kotlin Multiplatform (KMP) 项目中,要在桌面端(通常指 JVM 桌面应用,如 macOS, Windows, Linux)进行 JNI (Java Native Interface) 开发,核心思路是利用 KMP 的 expect/actual 机制,为桌面 JVM 平台提供 JNI 的实际实现。

JNI 简介

JNI 允许 Java 代码(或运行在 JVM 上的 Kotlin 代码)调用原生应用程序(用 C/C++ 等语言编写)或库,反之亦然。在 KMP 桌面端,这通常用于:

  • 集成现有 C/C++ 库: 如果你有成熟的原生库,JNI 是将其集成到 Kotlin 桌面应用的桥梁。
  • 访问平台特定功能: 某些操作系统级别的功能可能没有 JVM 或 Kotlin 友好的 API,此时可以通过 JNI 调用原生 API。
  • 性能敏感部分: 对于某些计算密集型任务,原生代码可能提供更好的性能。

KMP 桌面端 JNI 开发步骤

以下是使用 Kotlin Multiplatform 在桌面端进行 JNI 开发的详细步骤:

1. 设置 KMP 项目结构

确保你的 KMP 项目有一个 JVM 桌面模块。通常,你的 build.gradle.kts 文件会包含类似这样的配置:

// shared/build.gradle.kts
kotlin {
    jvm() // 这是针对桌面 JVM 平台的 target
    // ... 其他平台,如 android()

    sourceSets {
        val commonMain by getting {
            // ... common code
        }
        val jvmMain by getting {
            dependencies {
                // JNI 相关的依赖,通常在原生库编译时用到
                // 但在 Kotlin 代码中直接与 JNI 交互不需要额外的 Kotlin/JVM 依赖
            }
        }
    }
}

2. 定义 JNI 接口(Common Main)

commonMainexpect 类或接口中,定义你希望原生代码提供的功能。这与你在 Android 中使用 expect 声明平台特定功能的方式相同。

// shared/src/commonMain/kotlin/com/example/shared/NativeLib.kt

package com.example.shared

// 期望提供一个获取字符串的本地方法
expect class NativeLib() {
    fun getStringFromNative(): String
}

3. 实现 JNI 接口(JVM Main)

jvmMain 中,提供 expect 接口的 actual 实现。这个 actual 类将负责加载原生库,并声明 external 函数来映射到原生 C/C++ 方法。

// shared/src/jvmMain/kotlin/com/example/shared/NativeLib.jvm.kt

package com.example.shared

actual class NativeLib {
    // 静态代码块用于加载 JNI 库
    companion object {
        init {
            // "native_lib" 是你的 C/C++ 库的名称,不带前缀和后缀(例如 libnative_lib.so, native_lib.dll, libnative_lib.dylib)
            System.loadLibrary("native_lib")
        }
    }

    // 声明一个 external 方法,它会映射到 C/C++ 中的 JNI 函数
    external fun getStringFromNative(): String
}

4. 生成 JNI 头文件(.h

编译 jvmMain 模块,它会生成 .class 文件。然后,你可以使用 javah 工具(JDK 自带)或 javac -h 命令来生成 JNI 头文件。这个头文件定义了你需要用 C/C++ 实现的 JNI 函数签名。

步骤:

  1. 编译 jvmMain 运行 Gradle 命令编译你的项目,例如 gradlew :shared:compileJvmMainKotlin。这会在 shared/build/classes/kotlin/jvm/main/com/example/shared/NativeLib.class 生成类文件。

  2. 生成头文件: 打开终端,导航到你的项目根目录,然后执行以下命令。

    • 对于 Java 8 及更早版本,使用 javah
      javah -jni -d src/main/c++ shared/build/classes/kotlin/jvm/main/com/example/shared/NativeLib
      

      这里 src/main/c++ 是你希望存放头文件的目录。

    • 对于 Java 9 及更高版本,使用 javac -h
      javac -h src/main/c++ shared/src/jvmMain/kotlin/com/example/shared/NativeLib.jvm.kt
      

      javac -h 是直接从 .kt 源文件(实际上是 Kotlin 编译后生成 JVM 字节码的能力)生成 JNI 头文件,更方便。

    生成的头文件 (com_example_shared_NativeLib.h) 会包含类似以下内容:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_example_shared_NativeLib */
    
    #ifndef _Included_com_example_shared_NativeLib
    #define _Included_com_example_shared_NativeLib
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_example_shared_NativeLib
     * Method:    getStringFromNative
     * Signature: ()Ljava/lang/String;
     */
    JNIEXPORT jstring JNICALL Java_com_example_shared_NativeLib_getStringFromNative
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

5. 编写原生 C/C++ 代码

现在,你可以根据生成的 .h 头文件,编写 C/C++ 源文件(例如 native_lib.cpp),实现 Java_com_example_shared_NativeLib_getStringFromNative 函数。

// shared/src/main/c++/native_lib.cpp

#include "com_example_shared_NativeLib.h" // 包含生成的 JNI 头文件
#include <iostream> // 仅作示例

JNIEXPORT jstring JNICALL Java_com_example_shared_NativeLib_getStringFromNative
  (JNIEnv *env, jobject obj) {
    // 可以在这里调用其他 C/C++ 库或执行复杂逻辑
    std::string nativeString = "Hello from Native C++ in KMP Desktop!";
    std::cout << "Native C++ code executed!" << std::endl; // 打印到控制台,调试用
    return env->NewStringUTF(nativeString.c_str());
}

6. 编译原生库

你需要一个构建系统来编译你的 C/C++ 代码,并生成共享库文件(.so for Linux, .dylib for macOS, .dll for Windows)。常用的工具是 CMake 或 Gradle 的 Native Build 插件。

使用 CMake (推荐)

  1. 创建 CMakeLists.txt 在你的 shared 模块下创建一个 src/main/c++/CMakeLists.txt 文件。

    # shared/src/main/c++/CMakeLists.txt
    cmake_minimum_required(VERSION 3.10)
    project(native_lib CXX)
    
    # 查找 JNI 头文件和库
    find_package(JNI REQUIRED)
    
    # 添加你的 C++ 源文件
    add_library(native_lib SHARED native_lib.cpp)
    
    # 链接 JNI 库
    target_link_libraries(native_lib PRIVATE ${JNI_LIBRARIES})
    
    # 设置输出目录,方便 Gradle 查找
    set_target_properties(native_lib PROPERTIES
        LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" # 输出到 build/lib 目录下
    )
    
  2. 配置 Gradle 调用 CMake:shared/build.gradle.kts 中配置 cmake

    // shared/build.gradle.kts
    android { // 如果有 Android 平台,通常会在这里配置 externalNativeBuild
        // ...
        externalNativeBuild {
            cmake {
                path("src/main/c++/CMakeLists.txt") // CMakeLists.txt 的路径
            }
        }
    }
    
    // 或者为 JVM 目标单独配置 Native 构建(如果只有 JVM 桌面)
    // 通常在 desktop 或 jvm target 的 task 中调用 CMake
    tasks.register("buildNativeLib", Exec::class) {
        dependsOn("compileJvmMainKotlin") // 确保 NativeLib.class 已生成,方便 javac -h
        workingDir = file("src/main/c++") // CMakeLists.txt 所在目录
        commandLine("cmake", "-B", "build", "-DCMAKE_BUILD_TYPE=Release") // 配置 build 目录
        commandLine("cmake", "--build", "build") // 运行构建
    }
    
    // 确保在构建 JVM 应用时先构建原生库
    tasks.getByName("jvmMainClasses") {
        dependsOn("buildNativeLib")
    }
    
    // 或者更简洁地通过 Gradle 的 native 插件来管理,但更复杂
    // 或者直接在你的 desktop 模块的 run task 中手动复制库到 classpath
    

    注意: 对于桌面 KMP,最直接的方法是:

    • 手动运行 cmakecmake --build 命令来生成库。
    • 或者使用 Gradle 的 Exec 任务来自动化这个过程。
    • 然后将生成的 .so/.dylib/.dll 文件放置到 JVM 运行时能够找到的路径,例如:
      • 打包到 JAR 中(不太常见,因为需要特殊的 ClassLoader)
      • 放在 JVM 应用启动时 java.library.path 指定的目录。
      • 最简单的是,放在运行应用的目录的 libs 文件夹下,或者直接放在项目的 shared/src/jvmMain/resources 目录下,这样它会被打包进 JAR,但加载时可能仍需要 java.library.path。更推荐将原生库复制到最终可执行文件的同级目录。

7. 运行 KMP 桌面应用

在你的桌面应用模块(例如 desktop/src/main/kotlin/Main.kt)中,你可以像调用普通 Kotlin 函数一样调用你的 NativeLib

// desktop/src/jvmMain/kotlin/Main.kt (或其他桌面端入口文件)

import com.example.shared.NativeLib

fun main() {
    val nativeLib = NativeLib()
    val message = nativeLib.getStringFromNative()
    println("Message from native: $message")
}

运行注意事项:

  • 原生库路径: 当你运行桌面 JVM 应用程序时,JVM 需要能够找到你编译好的原生库文件。
    • 最常见且推荐的做法: 将生成的 .so, .dylib, .dll 文件放置在你的应用程序的可执行 JAR 文件所在的目录,或者一个名为 libs 的子目录中。
    • 通过 java.library.path 你可以在启动 JVM 时通过 -Djava.library.path=/path/to/your/native/libs 参数指定原生库的查找路径。
    • 在 Gradle 中打包: 某些 Gradle 插件可以帮助你将原生库打包到最终的可执行文件中(例如 jpackage 或一些 shadowJar 配置),但这会增加复杂性。

总结与建议

在 KMP 桌面端进行 JNI 开发是一个相对复杂的过程,因为它涉及到 Kotlin/JVM、C/C++ 和构建系统(Gradle, CMake)之间的协作。

关键点:

  1. expect/actual 这是 KMP 实现平台特定功能的基石。
  2. external 关键字: 告诉 Kotlin 编译器这个方法将由外部原生代码提供。
  3. System.loadLibrary()actual 实现中加载你的原生库。
  4. javah / javac -h 生成正确的 JNI 头文件,确保 C/C++ 函数签名正确。
  5. CMake 或其他原生构建系统: 用于编译你的 C/C++ 代码并生成共享库。
  6. 原生库部署: 确保 JVM 运行时能够找到你的 .so, .dylib, .dll 文件。

建议:

  • 只在必要时使用 JNI: JNI 会增加项目的复杂性(需要维护 C/C++ 代码,处理内存管理,平台兼容性等)。如果 Kotlin/JVM 本身可以完成任务,尽量避免使用 JNI。
  • 考虑 Kotlin/Native: 如果你的目标平台是完全原生的(例如,直接构建 macOS/Windows/Linux 可执行文件而不是 JVM 应用),Kotlin/Native 可能是更好的选择,它允许你直接调用 C 语言家族的库,而无需 JNI 的开销。但如果你需要利用 JVM 生态系统,JNI 是你的选择。
  • 逐步进行: 从一个简单的 JNI 调用开始,逐步添加更复杂的功能。
  • 查阅官方文档: JNI 和 Kotlin Multiplatform 的官方文档是最好的资源。

【跨平台】腾讯基于KMP实现的双鸿蒙方案介绍

【跨平台】腾讯基于KMP实现的双鸿蒙方案介绍

本文介绍了JVM平台上的try-catch机制实现方式及使用中的注意事项

在6月10号,腾讯开源了基于KMP实现的 ovCompose(online-video-compose) 开发框架,结合之前发布的的 Kuikly ,腾讯大前端团队 Oteam 已经开源了两种支持鸿蒙的一码多端跨平台方案。

原文:

Kuikly: 开源公告|Kuikly 跨平台开发框架

vcCompose: 重磅!支持纯血鸿蒙!腾讯视频ovCompose跨平台框架发布

在看过了他们的架构介绍之后,我对他们的实现原理进行了一个简单的总结。将其和Jetbrains的CMP对比又会有哪些异同呢?

之前的文章中,将CMP和Flutter、RN做了一个简单的横评:

Flutter & RN & CMP三种跨平台方案对比

Kuikly

触发编译

【跨平台】HarmonyOS鸿蒙系统应用层扫盲

【跨平台】HarmonyOS鸿蒙系统应用层扫盲

本文针对鸿蒙系统的基础特性,应用层开发所需基础知识,进行一个简单的调研,对比一下和其他平台的差异。

JS & TS & ArkTS

在HarmonyOS中,应用开发使用的官方语言是ArkTS语言,了解到它是基于 TypeScript 语言的扩展,旨在为开发者提供更高效、更安全的开发体验。

TypeScript 又是什么东西呢,对没有做过前端的开发来说,这几个语言的概念和特性会有点疑惑。

JavaScript (JS)

JavaScript是一种脚本语言,最初用于网页交互,现广泛用于前端、后端(Node.js)、移动端(React Native)等,现在已成为Web开发的主要语言。它是由网景公司(Netscape)在1995年开发的,最初被称为LiveScript,但后来改名为JavaScript,但是和Java没有关系。

JavaScript具有以下特点:

  • 动态类型:变量类型在运行时确定,无需显式声明。
  • 解释执行:浏览器或引擎(如V8)直接解释运行,无需编译。
  • 灵活但易出错:开发快速,但大型项目中维护困难(如类型错误需运行时才发现)。
// 动态类型
let num = 10; 
num = "hello"; // 合法,但易引发后续问题

// 函数式编程
const sum = (a, b) => a + b;

适用于网页动态交互(DOM操作);快速原型开发或小型项目;与HTML/CSS配合的前端开发(如React/Vue)。

TypeScript (TS)

TypeScript是JavaScript的超集,由微软开发,添加了静态类型系统,编译为JavaScript运行。

它具有以下特点:

  • 静态类型:需在代码中声明变量类型,编译时检查错误。
  • 编译为JavaScript:与JS兼容,可直接运行在浏览器或Node.js。
  • 面向对象支持:类、接口、泛型等高级特性。
  • 工具链完善:VS Code深度集成,提供智能提示和重构。
// 类型注解
let num: number = 10;
num = "hello"; // 编译时报错

// 接口与泛型
interface User { id: number; name: string; }
const getUser = <T>(id: T): T => id;

适用于大型前端项目(如Angular默认使用TS),以及需要高可维护性和团队协作的场景,还有后端开发(如NestJS框架)。

ArkTS

ArkTS是HarmonyOS生态的专属语言,由HarmonyOS官方提供,融合了TS的静态类型系统和HarmonyOS的声明式UI、状态管理等特性。ArkTs和TS,有点类似Jetpack Compose和Kotlin的关系。

它具有以下特点:

  • 静态类型:与TS一致,编译时检查错误。
  • 声明式UI:提供类似React/Vue的声明式API,简化UI构建。
  • 分布式能力:直接调用HarmonyOS API(如分布式软总线)。
  • 高性能字节码:基于Ark运行时(类似JVM),性能优异。
// 声明式UI
@Entry
@Component
struct MyComponent {
  build() {
    Column() {
      Text("Hello, HarmonyOS!");
      Button("Click Me", () => { console.log("Button Clicked"); });      
    }
  }
}

适用于HarmonyOS应用开发,特别是需要高性能、分布式能力的场景,如智能穿戴设备、智能车等。

应用运行环境

Android

在Android平台上,每个应用程序都运行在一个独立的进程中,由Android Runtime(ART)虚拟机提供执行环境。

ART是基于寄存器架构的高级虚拟机,其设计继承自Dalvik虚拟机但进行了全面重构,采用AOT 预先编译技术将DEX字节码预先编译为本地机器码。

这是一个 类JVM环境 ,通过优化过的指令集执行编译后的代码,同时保持与Java SE标准库的部分兼容性(通过Android SDK提供的精简实现)。

在系统架构层面,应用进程通过 Binder 这个 IPC 机制与Android系统服务进行进程间通信。Binder作为基于Linux内核的高效通信框架,采用内存映射和引用计数技术实现跨进程方法调用(RPC),其传输性能比传统IPC方式提升5-10倍。

例如 AMS(ActivityManagerService) 通过Binder向应用进程发送生命周期控制指令,而应用则通过代理接口(如IActivityManager.Stub)回调系统服务,形成完整的进程间协作机制。

应用运行时,ART虚拟机通过以下核心机制实现资源管理:

  • 内存管理采用分代垃圾回收(Generational GC)策略,配合Android特有的 Low Memory Kill 机制
  • UI线程使用消息队列(MessageQueue)和Looper实现事件驱动模型
  • 图形渲染通过 RenderThreadSurfaceFlinger 服务协同,采用 VSync 信号同步的帧调度机制
  • 资源加载通过 Resources 类实现多维度(dpi/语言/方向等)资源匹配系统

这种架构设计使得Android应用在性能受限的移动设备环境下,仍能实现高效的进程隔离、资源调度和用户体验保障。最新的ART改进(如从Android 12引入的压缩引用指针技术)进一步将内存占用降低20%,体现了持续优化的技术演进路径。

IOS

在iOS平台上,每个应用程序都运行在一个独立的沙盒环境中,由Objective-C/Swift运行时(Runtime)提供核心执行能力,并基于Mach-O可执行格式直接在ARM架构上运行原生代码。不同于Android的虚拟机机制,iOS采用AOT(Ahead-Of-Time)全量编译,通过LLVM编译器链 将代码预先优化为机器指令 ,结合苹果自研的A系列芯片的定制指令集(如Apple Silicon的AMX矩阵加速指令),实现接近裸金属的执行效率。

LLVM(Low Level Virtual Machine)是一个模块化、可重用的编译器和工具链技术集合的开源项目。尽管它的名字中带有“虚拟机”,但它与传统的虚拟机关系不大,而是一个专注于提供现代编译器基础架构的平台。它是一个编译器基础设施项目,提供了一套灵活、可扩展的工具和库,使得开发人员可以轻松地构建自己的编译器、调试器、JIT 编译器和其他程序分析和转换工具。它的模块化设计和强大的优化能力使其在现代软件开发中扮演着越来越重要的角色。Swift官方编译器就是基于 LLVM 构建的。

系统架构与进程通信

iOS通过XNU混合内核(融合Mach微内核与BSD特性)实现系统级隔离,应用与系统服务的交互主要依赖以下机制:

  • Mach IPC:基于端口(port)的轻量级进程通信,延迟低于1微秒,用于底层服务调用(如进程生命周期通知)
  • XPC:高层抽象通信框架,采用序列化对象(NSXPCConnection)实现沙盒间的安全数据交换
  • Darwin Notify:基于notifyd服务的跨进程事件广播系统,用于处理系统状态变更(如低内存警告)

核心运行机制

内存管理
  • 采用自动引用计数(ARC)编译时内存管理,通过插入retain/release调用替代垃圾回收
  • 使用Jetsam机制主动终止内存超限进程,配合内存压缩(iOS 13+)降低OOM概率
UI渲染流水线
  • Core Animation合成器直接驱动GPU渲染(Metal API),通过CADisplayLink实现120Hz ProMotion自适应帧同步
  • UIKit主线程遵循严格的RunLoop事件模型(NSDefaultRunLoopMode),所有UI操作必须通过dispatch_async(dispatch_get_main_queue())提交
资源管理
  • Asset Catalog编译时自动生成多分辨率@2x/@3x图像集,并支持按设备型号动态加载
  • dyld共享缓存将系统库预链接为内存映射文件,减少应用启动时的符号解析开销

性能优化特性

  • Swift Runtime使用值类型(Value Types)减少堆分配,方法派发采用直接调用(非ObjC消息转发)
  • GCD(Grand Central Dispatch):基于线程池的优先级队列(QoS级别),支持硬件线程感知的任务调度
  • MetricKit:实时监控CPU/内存/电池消耗,通过signpost日志实现纳秒级性能分析

这种架构使得iOS应用在保持严格安全隔离的同时,仍能实现亚毫秒级响应(如相机启动仅需650ms)。随着Swift 6引入并发模型和actor隔离机制,系统进一步强化了多核环境下的线程安全能力,体现了苹果软硬协同设计的持续进化。

HarmonyOS

鸿蒙系统也可以说是源于AOSP,基于Android10的开源代码演进而来。在 HarmonyOS(鸿蒙系统) 中,应用程序的运行环境与 Android 的 ART 虚拟机不同,它采用了多模式混合执行架构,具体取决于应用类型和部署场景。有以下几种类型:

传统应用(兼容Android APK)—— 运行在 Ark Runtime(方舟运行时)

类似ART但优化更彻底,使用 方舟编译器(Ark Compiler) 对 Java/JS 代码进行 AOT(Ahead-of-Time)静态编译,生成高效机器码,避免 JIT(即时编译)带来的性能波动。内存管理采用 智能分代回收(Generational GC),相比 ART 减少 20% 的 GC 停顿时间。支持多实例隔离(如同时运行两个微信),每个实例在独立沙盒中运行。

原生鸿蒙应用(基于 ArkTS/JS/Java)—— 运行在 Ark Engine(方舟引擎)

轻量级高效运行时,ArkTS代码会被编译为字节码(Ark Bytecode, ABC),由 Ark Engine 解释执行(未来可能支持 AOT)。无虚拟机开销:相比 JVM,Ark Engine 直接操作内存和硬件资源,减少抽象层带来的性能损失。关键任务(如 IoT 设备控制)可绑定 CPU 核心,确保实时性(延迟 <10ms)。

Native应用(C/C++/Rust)—— 直接运行在 Linux/微内核

适用于高性能场景(如游戏、驱动),代码会直接编译为 ELF 可执行文件,由 Linux 内核(标准版鸿蒙)或 LiteOS-A(轻量版鸿蒙) 调度。通过 HDF(硬件驱动框架) 访问硬件,无需经过虚拟机或运行时层。

跨设备分布式应用 —— 运行在“软总线”虚拟化环境

应用组件可跨设备动态迁移。例如,手机上的视频播放可无缝切换到智慧屏,底层通过 DSoftBus(分布式软总线) 实现 IPC 通信(延迟 <20ms)。资源按需加载,不同设备共享同一份应用逻辑,但渲染和计算可能分布在多个终端。

HarmonyOS系统架构

简要对比完Android和IOS的应用层架构,我们再来看下鸿蒙系统的应用层架构。

这套架构主体分为应用、框架、引擎以及跨平台适配这几部分,应用层就是透出给开发者的语法,有好几种模式,下文详解。框架层实现了前端框架常见的组件化、MVVM 能力,能够响应式的更新 UI。下面是 JS 引擎,使用的是 QuickJS,应该也支持 V8。再向下是渲染引擎,包含了核心的渲染管线、动画、事件和各种布局绘制算法。 最下面的 porting layer 是适配多平台的关键,定义了平台无关的 layer 数据结构,可以提交给不同的合成器(Compositor)合成渲染,从代码上看,也是支持 Flutter Engine 的。

首先鸿蒙应用是打包成 HAP (Harmony Ability Package) 格式分发的,和安卓 APK 一样都是压缩包,包结构大同小异。分发到端上之后,统一由鸿蒙(概念)的 API 承接,然后就分了不同的模式。在华为手机上推送的鸿蒙版本,可以无感兼容安卓应用,肯定是依赖了 AOSP 的。从Harmony4.0开始,​​官方正式宣布终止 APK 兼容模式​​,系统层移除对 Android 生态的依赖(如不再内置 ART 虚拟机)。仅支持运行 ​​纯鸿蒙应用(.hap 格式)​​ 或通过 ​​方舟编译器静态转译的 Android 应用​​(需重新打包为 .hap)。并且,在​OpenHarmony 3.2+(开源分支)​​从内核层面移除 Linux 兼容层,仅保留 ​​LiteOS-A 微内核​​,彻底无法运行 Android 应用。

学习转载自:HarmonyOS ArkUI 框架的实现原理和落地实践

要设计操作系统以及打造一个框架,一定是重新研发的,而不是在现有平台基础之上修改, UI 也是重新设计的,它的设计目标总结起来基本包含这么几个方面。

  • 第一点要能够实现跨多端,多设备,还有多尺寸的屏幕;
  • 第二点是能够实现一套代码适配多终端设备;
  • 第三点是一次开发多端部署,要能保证一致的设计和开发风格;
  • 第四点是不使用 Java,也不使用 ART,因为鸿蒙操作系统在去 Java 化,基本上不再使用 Java 了;
  • 第五点要能保证一致的事件和动效的处理机制;
  • 最后一点要支持多语言和高效的语言运行时,而且还要支持在多种不同的设备上使用。

【跨平台】使用AYA项目Server重构DebugManager工具的应用管理

【跨平台】使用AYA项目Server重构DebugManager工具的应用管理

本文记录了跨平台Android设备调试工具DebugManager,在使用AYA项目重构应用管理功能的流程与分析

这个工具绝大部分功能都是基于 adb 命令来实现的,在ProcessBuilder中执行命令,获取输出流,对输出的内容进行解析,在结构化地展示到界面。

然而没有直接获取应用图标文件这样的命令,只能从android内部下手,获取png文件再发送到Desktop端。本文介绍一下应用管理功能,图标文件获取这个实现的发展历程和最新的使用AYA服务端更改的版本。

发展历程

一、车机阶段

这个工具在最初开发的时候,是面向车机平台。

由于我可以获取到我们平台的系统签名文件,所以是直接编写了一个带系统签名的APP,内部有一个服务,在 onCreate() 中获取安装所有的应用信息:

  • label
  • packageName
  • versionName
  • versionCode
  • lastUpdateTime

最麻烦的需要存储的是应用的ICON图标文件,因为想要在电脑端的JVM应用 DebugManager 中显示应用的图标,是没有adb命令可以使用的,只能从Android平台上获取到 Bitmap 数据,转换为png存储下来。最后拉取到Desktop端,读取文件显示图标。

编写玩服务代码,在安装这个APK后,通过 am startService 来运行这个服务,服务端获取信息,将所有信息准备好后再存储到应用内部路径的文件中。

/data/data/com.stephen.appinfoservice/files/appinfo.json
/data/data/com.stephen.appinfoservice/icons/*.png

二、手机阶段

在面对更普遍的手机端的应用管理功能时,我发现了没有系统签名的应用,是不可以直接使用 am startservice 命令来拉起服务的。而且不同手机平台还有权限上面的区别,比如类原生的系统上(Pixel平台,LineageOS),是不用动态申请就可以获取安装的应用信息,但是在国产OS上,有更严格的管理,需要处理读取应用列表权限。

当然可以专门做一个Activity来交互,让用户同意读取应用列表的权限。但是我更希望这个过程是无感的。

所以还是要探索其他的办法。

AYA项目服务端

AYA也是一个基于adb命令来显示Android设备信息,进行调试的项目,同样为跨平台的产品形态。

它是使用比较火的Electron框架,TypeScript 语言编写的。看到他们有类似的应用管理页面,可以显示APP图标,我想他们肯定也是在Android设备上有一个服务端的。看看他们服务端的实现方式,并将其改写成适配 DebugManager 项目的数据传输方案。

AYA的服务端不是承载于一个普通的Android应用进程上,而是类似于一个DEAMON守护进程。

代码运行分析

这是一个 Android 应用,但它并不是一个常规的 APK,而是一个在 Android 设备上通过 dex 文件直接运行的程序。这种方式通常用于需要更高权限或者直接访问系统服务的工具类应用,类似于 adb shell 上的一个服务。

在项目的Gradle脚本中,配置了一个任务,这个任务会将项目的代码build生成的apk里面的 dex 文件提取出来,然后通过 adb 命令,推送到设备的 data/local/tmp 路径下,使用 CLASSPATH 指定运行。

android.applicationVariants.all { variant ->
    variant.outputs.all {
        outputFileName = "aya-server-v${versionName}.apk"

        def dexPath = rootProject.rootDir.path
        variant.assembleProvider.get().doLast {
            copy {
                def file = zipTree(file(outputFile)).matching { include 'classes*.dex' }.singleFile

                from file
                into dexPath
                rename { String fileName ->
                    fileName.replace(file.getName(), "aya.dex")
                }
            }
        }
    }
}

从整个流程看,这个项目是一个用于在 Android 设备上获取和管理应用信息的工具。它利用了 LocalSocket 进行进程间通信,通过 反射 访问 Android 隐藏的 API 来获取详细的系统和应用信息,并将这些信息缓存到设备的文件系统中。

对原来的代码进行了改写,AYA项目继承了protobuf通信框架,直接将所有的应用信息,全部通过protobuf协议进行传输,包括图标文件(base64编码后)。

protobuf是Google开发的一种语言无关、平台无关、可扩展的序列化数据结构的方法,它可以将数据结构序列化为二进制格式,用于在网络上传输或存储。

我选择了更简单的通信方案,直接使用JSON字符串,在客户端使用Kotin序列化解析。然后服务端在运行时,将图标png存储到固定位置,拉取到电脑端再显示。这种方式打包的dex文件体积缩小了90%。

代码简要分析:

服务入口 Server.kt

Server.ktmain 方法是整个程序的入口点。

fun start(args: Array<String>) {
    Log.i(TAG, "Start server")

    val server = LocalServerSocket("aya")
    Log.i(TAG, "Server started, listening on ${server.localSocketAddress}")

    while (true) {
        val conn = Connection(server.accept())
        Log.i(TAG, "Client connected")
        executor.submit(conn)
    }
}
  • Server.main 方法被调用,创建一个 Server 实例并调用其 start 方法。
  • start 方法首先创建一个名为 "aya"LocalServerSocketLocalSocket 是 Android 上一种特殊的 IPC(Inter-Process Communication,进程间通信)机制,它允许同一个设备上的不同进程通过本地套接字进行通信。
  • 服务器进入一个无限循环 while (true),等待客户端连接。
  • 每当有客户端连接时 (server.accept()),服务器会创建一个新的 Connection 实例,并将该实例提交给一个缓存线程池 executor 来执行。这意味着每个客户端连接都会在一个独立的线程中处理,从而实现了并发处理。

请求处理 Connection.kt

Connection.kt 负责与单个客户端进行通信。

  • run 方法获取客户端的输入流和输出流,用于读取请求和发送响应。
  • 它进入一个循环,从输入流中读取客户端发送的 JSON 字符串请求。
  • 读取到的 JSON 字符串被解析,然后调用 handleRequest 方法来处理具体的请求。

handleRequest 方法根据请求中的 method 字段来分发不同的处理逻辑。

when (method) {
    "getVersion" -> {
        put("version", getVersion())
    }

    "getPackageInfos" -> {
        put("packageInfos", getPackageInfos(JSONObject(params)))
    }

    "saveAllInfoToFile" -> {
        put("saveResult", saveAllInfoToFile(params))
    }

    else -> {
        Log.e(TAG, "Unknown method: $method")
        put("error", "Unknown method: $method")
    }
}

调用的 getPackageInfo 方法是核心逻辑,它利用反射机制获取系统服务并查询应用信息。

  • 获取系统服务:
    • 通过 ServiceManager.packageManagerServiceManager.storageStatsManager 获取 PackageManagerStorageStatsManager 的实例。
    • ServiceManager.kt 中使用反射调用了隐藏的 android.os.ServiceManager.getService 方法,从而获取系统的 IPackageManagerIStorageStatsManager 服务。这种方式通常需要特殊的权限或者在 root 环境下才能成功。
  • 查询信息:
    • 使用 ServiceManager.packageManager.getPackageInfo 获取 PackageInfo 对象,其中包含了应用版本、安装时间等基本信息。
    • PackageInfo 中获取 ApplicationInfo,进而获取应用的 apkPathflags。通过 flags 判断应用是否为系统应用。
    • 如果设备版本在 Android 8.0(Oreo)及以上,会使用 ServiceManager.storageStatsManager.queryStatsForPackage 获取应用的存储统计信息,包括应用数据大小、缓存大小等。
  • 获取应用名称和图标:
    • 利用 apkPath,通过反射创建 AssetManager 并加载 APK 资源,然后获取应用的名称(label)和图标(icon)。
    • 如果图标存在,会将其转换为 PNG 格式并保存到 /data/local/tmp/aya/icons 目录下。
  • 组织和返回数据:
    • 所有获取到的信息都被封装成一个 JSONObject 返回。

生命周期与数据流转

普通 Android 应用(APK)的生命周期是由系统 PackageManagerActivityManager 严格管理的。而这个服务端则是一个 “裸” 进程

它的生命周期管理方式更接近于一个传统的 Linux 守护进程(daemon)。它没有标准的 Android 应用入口点(如 Launcher Activity)。它通常需要通过 adb shell 或其他特殊工具(如 Magisk 模块)来手动启动,命令通常类似于 app_process /system/bin io.liriliri.aya.Server

它的生命周期完全由启动它的进程控制。只要启动它的 shell 进程或父进程不被终止,这个服务端进程就会一直运行。它没有 onStoponPause 等 Android 生命周期回调。由于它不是一个常规的应用进程,系统不会像管理普通应用那样主动去管理它。如果它没有被其他应用组件绑定,系统通常不会轻易终止它,除非设备内存极度紧张。

使用 反射 来调用系统隐藏的 API 。这种方式绕过了标准的权限检查。

这个服务端运行在一个单独的 JVM(Java Virtual Machine)进程上。

具体来说:

  • 独立的进程: AYA的服务端代码有一个 main 方法在 Server.kt 中。在 Android 平台上,当一个 Java/Kotlin 程序通过 CLASSPATH 运行并带有 main 方法时,它会被系统启动为一个独立的进程。这个进程会拥有自己的 Dalvik 或 ART(Android Runtime)虚拟机实例。
  • 本地套接字服务器: 这个进程创建了一个 LocalServerSocketLocalSocket 是一种 Android 内部的 IPC(进程间通信)机制,它允许同一个设备上的不同进程进行通信。你的服务端进程会监听这个套接字,等待其他客户端进程(例如,一个通过 adb shell 启动的客户端,或者一个单独的 Android 应用)来连接。

服务端程序可以被看作是一个后台服务,它作为一个独立的进程在 Android 系统中运行,并通过本地套接字提供服务。运行成功后,再通过 adb forward tcpip:xxxx localabstract:aya 命令,把这个设备内部的本地套接字通信,映射到电脑的 TCP/IP 端口上。

以下是完整的通信流程:

  • 你的电脑上的客户端程序(例如,一个 Python 脚本或一个 C++ 程序)会尝试连接到 localhost:xxxx。这个连接请求是一个标准的 TCP/IP 连接请求。
  • adb 发现一个连接到你电脑 xxxx 端口的请求时,它会拦截这个请求。adb 作为一个桥梁,将这个 TCP/IP 流量通过 USB 数据线或 Wi-Fi 发送到你连接的 Android 设备。
  • 设备端的 adb 守护进程(adbd)接收到这个连接请求。它会识别出这个请求是为 localabstract:aya 设定的转发规则。
  • adbd 守护进程在设备上作为一个新的客户端,主动去连接 localabstract:aya 这个本地套接字。
  • Server.kt 里的 LocalServerSocket 正在监听 localabstract:aya。当 adbd 发出连接请求时,server.accept() 会返回一个与 adbd 建立连接的 LocalSocket

基于以上分析路线,电脑上的客户端发送的所有数据,都会通过以下路径传输:

PC 客户端PC 的 adbUSB 数据线/Wi-Fi设备上的 adbdlocalabstract:aya 本地套接字你的服务端进程 (Server.kt)

同时,Android服务端发送的响应数据也会通过这条路径原路返回。

这种方式允许你像调试一个常规的网络服务一样,直接在电脑上与运行在 Android 设备上的程序进行交互。你不需要在设备上安装一个完整的网络服务器,也不需要担心防火墙或其他网络配置问题。adb 巧妙地为你解决了跨进程、跨设备的通信问题,将本地 IPC 流量无缝地转发到了你的电脑上。

DebugManager对接

电脑端的代码,在数据处理上几乎没有改动,将原来的安装apk流程和启动服务流程,改为了推送dex文件到设备,使用

CLASSPATH=/data/local/tmp/aya/aya.dex app_process /system/bin io.liriliri.aya.Server

来启动服务,再通过adb forward tcpip:xxxx localabstract:aya命令,把这个设备内部的本地套接字通信,映射到电脑的TCP/IP端口上。

DebugManager内部,通过 adb shell pm list packages 命令,获取到所有安装的应用列表。解析出每一个包名,再调用服务端的 getPackageInfo 方法,获取应用label,版本信息。最后通过coil的AsyncImage组件,填入icon文件路径,异步加载图标。

界面升级

此前的交互也进行了同步升级,再有限的窗口内展示更多的信息,全局缩小了字号和模块之前的padding,将列表类型改为了图标矩阵,使用 LazyVerticalGrid 组件来承接应用图标展示。

详细信息弹窗:

同时为了缩小重组范围,使用应用的packageName作为key,来标识每一个item,还可以以此来实现每一个item的移动动效,比图标的体验更丝滑。

LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 105.dp)) {
    items(appListState.sortedBy { it.label }, key = { it.packageName }) {
        Box(
            Modifier.animateItem(
                fadeInSpec = null,
                fadeOutSpec = null,
                placementSpec = tween(300)
            )
        ) {
            GridAppItem(
                label = it.label,
                iconFilePath = mainStateHolder.getIconFilePath(it.packageName),
                modifier = Modifier.padding(5.dp)
                    .size(100.dp).clip(RoundedCornerShape(10))
                    .padding(5.dp)
                    .bounceClick().clickable(
                        indication = null,
                        interactionSource = remember { MutableInteractionSource() }
                    ) {
                        dialogInfoItem.value = it
                    },
                onClickShowInfo = {
                    dialogInfoItem.value = it
                },
                onClickOpen = {
                    mainStateHolder.startMainActivity(it.packageName)
                },
                onForceStop = {
                    mainStateHolder.forceStopApp(it.packageName)
                },
                onExtractApk = {
                    mainStateHolder.pullInstalledApk(it.packageName, it.versionName)
                },
            )
        }
    }
}

在动效方面,直接使用 animateItem 函数就可以实现列表项的移动动效。

Modifier.animateItem(
    fadeInSpec = null,
    fadeOutSpec = null,
    placementSpec = tween(300)
)

【跨平台】Kotlin Multiplatform闲谈

【跨平台】Kotlin Multiplatform闲谈

本文介绍了Kotlin Multiplatform框架过去现在和未来的一些讨论主题。

论题来自于 霍丙乾(Benny Huo) 在B站上的答网友问。基于其描述,详细扩展开来。

有浏览器H5套壳,为什么还要用Kotlin跨端

H5 主要是使用 Web 技术(HTML、CSS、JavaScript)来构建应用程序,然后通过 WebView(一个内嵌的浏览器组件)在不同平台的原生应用中运行。常见的 H5 跨平台框架包括 Ionic、PhoneGap (Cordova)、以及一些基于小程序(如微信小程序)的开发方式。

实现上是将 Web 应用打包成原生应用,通过 WebView 渲染界面和执行逻辑。由于运行在 WebView 中,性能通常不如原生应用,尤其是在处理复杂动画、大量数据或需要高性能计算的场景。启动速度也不如原生应用。对原生功能的访问需要借助插件。

Kotlin Multiplatform (KMP) 是 JetBrains 推出的一项技术,它允许开发者使用 Kotlin 语言编写共享的业务逻辑(如数据处理、网络请求、商业规则等),并将其编译成适用于不同平台的原生代码。UI 层通常仍然使用各平台的原生技术(Android 的 Jetpack Compose/XML,iOS 的 SwiftUI/UIKit)来实现,但也支持使用 Compose Multiplatform 实现共享 UI。

Kotlin编译器会将面向不同平台的Kotlin代码直接 编译成对应平台的Native代码实现 ,可以说在每个平台上都是 原生应用 ,从性能上来说媲美原生。如果进一步使用Compose Multiplatform,则可以在各个平台上共享一套UI代码,使用 Skia 跨端渲染引擎,其性能也远高于 H5 开发。在开发周期上比 H5 长一点。体量大一点的应用一般都会追求更长远,性能更好的技术

总结起来就是:

  • H5 跨平台 更像是“将网页打包成应用”,优势在于开发速度快、Web 开发者门槛低,但性能和原生体验是其短板。
  • Kotlin Multiplatform 更像是“用 Kotlin 编写原生应用的一部分”,优势在于性能接近原生、能充分利用原生特性,并且可以灵活选择共享业务逻辑或 UI,但对开发者有一定的 Kotlin 基础要求,且 UI 部分(如果选择原生)仍需单独开发。

Kotlin/Native性能对比其他语言如何

Kotlin/Native 旨在将 Kotlin 代码编译为可以在没有虚拟机 (VM) 的情况下运行的本地二进制文件,使其适用于嵌入式设备或 iOS 等平台。Kotlin/Native 代码直接编译为机器码,因此它的执行速度通常非常快,可以与原生应用媲美。对于 CPU 密集型任务,其性能通常远优于 JVM 上的 Kotlin 或 JavaScript。

Kotlin/Native 包含一个现代的跟踪垃圾回收器。虽然自动内存管理简化了开发,但在某些情况下,GC 可能会引入微小的暂停,这可能会影响对实时性要求极高的应用。不过,JetBrains 正在不断优化其内存管理器。

同Swift相比,Kotlin不会自动回收,内存消耗会高一点,这使得其运行起来反而更快。对象在内存管理级别做了池化,创建和使用都会比Swift更快。

与 C/C++ 相比,它在以下几个方面通常存在差异。

C/C++: 提供对内存的直接控制(通过指针)、更细粒度的硬件优化(如 SIMD 指令、CPU 缓存优化)以及手动内存管理。这使得 C/C++ 成为需要极致性能和资源控制的场景(如操作系统、驱动程序、游戏引擎、高性能计算)的首选。

Kotlin/Native: 虽然性能接近原生,但它仍然是高级语言,抽象层高于 C/C++。它提供了垃圾回收机制,简化了内存管理,但也意味着开发者对内存布局和生命周期的直接控制较少。在需要极度细致的内存布局和手动优化以榨取每一丝性能的场景下,C/C++ 仍然更具优势。

非Android平台为什么不使用Kotlin/Native替代JVM

以Android平台为例,如果使用 Kotlin/Native 去绕过Android虚拟机,那么开发上,Google的api都不可以直接使用了,仍然需要套一层壳。

在移动端的另外两大平台上,KMP在开发时,是采用了类似RN的桥接调用的,腾讯在将Kotlin/Native移植到鸿蒙系统时,就是使用ArkTS包装鸿蒙的系统api,给Kotlin调用,包括IOS上也是如此实现。

对于桌面端,Compose Desktop 仍然是运行在JVM上。Windows、MACOS、Linux等桌面系统底层的差异性,想抹平是非常困难的。

目前桌面端最火的 Electron 框架为例,Electron 应用通常需要独立运行,不能依赖用户本地环境的 Chromium 或 Node.js 版本,都会自己打包一个 V8 引擎,负责解析、编译和执行应用中的 JavaScript 代码(包括前端页面逻辑和 Node.js 后端代码)。正是因为有这个附带的引擎,安装包体积高达几百M,但是其能提供的开发体验和跨平台生态是更应该关注的点。

同理,Compose Desktop没有选择直接对接操作系统,而是选择运行于 JVM 上,也是由于JVM已经把平台适配给做完了,并且提供的接口和性能已经过多年发展验证,可以很好地支持快速开发和功能达成。包括前段时间的Rust,甚至是基于浏览器来运行。

如果不谈Compose UI界面,Kotlin本身其实是可以通过Kotlin/Native直接跑在各个桌面平台上的,比如Windows端通过 Mingw(Windows系统api封装中间层)。另外也可以通过GTK来实现UI界面。

GTK(GIMP Toolkit) 是一个开源的跨平台 图形用户界面(GUI)工具库,最初为图像处理软件 GIMP 开发,现广泛用于 Linux 桌面环境(如 GNOME)以及其他操作系统(Windows、macOS)。GTK 本身是用 C 编写的,和 Kotlin 无法直接交互,Binding 充当了“桥梁”,将 GTK 的 C 语言接口通过某种方式(如 JNI、FFI 或原生库)暴露给 Kotlin,使 Kotlin 开发者能直接调用 GTK 的功能来构建 GUI 应用。

Compose IOS 的一些坑

Kotlin 最开始是和OC互调用的,后来的2.1版本才开始转向和 Swift 互调用。IOS开发端,Cocapods 处于不维护的存量状态,整体在往Swift生态迁移,后面会全面转向Swift了。

腾讯视频 在IOS上自研了一套渲染引擎,因为要兼容大量的原生存量代码,前期只能小范围替换,省掉了一个渲染层的内存。在鸿蒙端是纯以来CMP的Skia了。

  • 混合开发的时候,内存,画布开销。
  • 单独的View容器需要自己管理生命周期。
  • 列表组件单独嵌入到原生容器,可能开发上会比较麻烦,处理手势。

和 OC 互操作方面,类的导出有很多限制。和Swift应该差不多,也会有这些限制。编译的时候不会发现,运行时才知道,

霍老师在腾讯云开发者平台上发布的Kotlin/Native的文章,详细列出了这些限制的问题。

深入理解Kotlin/Native

互操作尽可能少,导出一些工具函数等。

能做到三大移动端一把梭哈吗

前段时间 Jetbrains 发布通知,CMP的IOS端已经稳定。

在鸿蒙端,腾讯视频团队已经实现了比较稳定的方案,在和Jetbrains沟通能否贡献到官方代码中。

所以技术上Android、IOS、鸿蒙三端共用是没有问题的。

CMP和RN和Flutter的对比

此前已经对比过一次三者的易用性,性能,渲染方式上的差异。 差异。

Kotlin Multiplatform 对比 React Native 和 Flutter

CMP最大的优势还是来自于Kotlin,在各个平台上,编译完后都是Native实现。另外在UI渲染上采用Skia,性能优于RN,持平Flutter。

能否替代Flutter?

有潜力,但是CMP的潜力不止于替换Flutter,Kotlin编译器编译完的代码,在框架上和原生开发无异,胃口大一点问问是否可以替代原生。

每个平台都有最合适的原生代码,CMP三段一码的开发成本,对大型app诱惑还是很大,成本和收益都是需要考虑的。

CMP前景如何

CMP前景依托于Kotlin跨端的前景。

Kotlin跨平台是语言特性,而不是框架,并没有做一个中间层来转换。

Kotlin中可以直接访问C的结构体。

以Java生态要求Kotlin,还是有很长的路要走。

Compose的WebAssembly,也是 Jetbrains 官方下一阶段的重点,IOS端已经稳定。

相比其他的框架,其问题在于正是因为到处都是Native,就需要 对这个target的原生环境有一定了解 。知道如何去调用原生的API,比如处理内存,处理生命周期,处理手势等。

在IOS上写代码,就需要对Swift有一定了解,依赖原生api去配置。

ovCompose

腾讯视频团队推出的跨平台开发框架ovCompose以及相关基础库KuiklyBase,旨在解决跨平台开发中的一些痛点问题,并推动Compose跨端生态的发展。

项目背景

  • 跨平台需求:随着鸿蒙系统的推出,客户端跨平台开发的需求显著增加。传统的UI跨端方案已无法满足业务需求,全跨端APP(覆盖Android、iOS和鸿蒙)的开发成为降低开发成本和提升效率的关键。
  • 现有技术的局限性:尽管Kotlin Multiplatform具备高性能和灵活的原生交互能力,但它存在一些问题,如不支持鸿蒙系统、iOS混排能力受限以及GC性能表现一般等。

ovCompose和KuiklyBase的特性优势

  • 鸿蒙高性能:KuiklyBase选择了Kotlin Native(KN)作为鸿蒙适配方案,相比JavaScript(JS)具有更快的执行速度和更好的三端一致性。通过性能优化,如内联优化、ThreadLocal优化等,显著提升了执行效率。
  • 鸿蒙三明治架构支持混排:利用Skia渲染方案和XComponent组件,解决了Compose与原生组件的混排问题,支持粘贴按钮等安全组件的混排。
  • 三端高一致性:通过Kotlin Native方案解决了跨线程访问问题,保持了逻辑运行的一致性。同时,iOS和鸿蒙平台均采用Skia渲染,确保了UI绘制的一致性。
  • iOS多模态渲染:采用指令映射方案,解决了Compose在iOS上的混排难题,并实现了与原生UI的灵活混排。
  • Kotlin Native内存优化:包括GC优化(如GC抑制、分段GC、Sweep优化)和堆Dump优化,显著提升了内存管理效率。
  • KuiklyBase组件生态:提供了丰富的组件,如异常堆栈还原组件、跨语言互调用组件、资源管理组件、原子操作组件、协程组件、序列化组件等,为开发者提供了强大的支持。

实现原理

  • KN鸿蒙平台适配:通过在Kotlin IR转LLVM IR时使用苹果的LLVM 11,在LLVM IR生成可执行文件时使用鸿蒙的LLVM 12,解决了鸿蒙平台的适配问题。
  • KN性能优化:包括内联优化、ThreadLocal优化、协程性能优化、调试性能优化等,显著提升了Kotlin Native在鸿蒙平台上的性能。
  • 鸿蒙绘制不同步问题解决:通过采用XComponent的Texture模式,解决了Compose与ArkUI绘制不同步的问题。
  • iOS多模态渲染:设计了基于iOS的PictureRecorder局部更新架构,通过增量hash减少hash计算量,优化了绘制效率。

开源信息

开源仓库:ovCompose和KuiklyBase已在GitHub开源,包含5个仓库,地址为:https://github.com/Tencent-TDS

未来计划

  • 持续优化:重点优化GC在业务场景的表现、Kotlin-Native组件化、开发体验优化以及UIKit渲染模式与Skia的进一步对齐。
  • 扩展支持:计划将ovCompose和KuiklyBase扩展到TV和PC端,进一步完善跨平台开发框架。

与KuiklyUI的差异

  • KuiklyUI:侧重于静态化+动态化双运行模式,采用轻量原生渲染,支持H5和小程序。
  • ovCompose:专注于全面对齐Compose Multiplatform标准API,采用自渲染方式实现鸿蒙平台的适配,确保三端高度一致性。

【跨平台】使用Compose Multiplatform开发跨平台的Android调试工具

【跨平台】使用Compose Multiplatform开发跨平台的Android调试工具

使用Compose Multiplatform开发了一款跨平台的电脑端Android设备调试工具。本文简单介绍了开发背景,功能点,特殊问题解决等信息。

背景

最近对CMP跨平台很感兴趣,为了练手,在移动端做了一个Android和IOS共享UI和逻辑代码的天气软件,简单适配了一下双端的深浅主题切换,网络状态监测,刷新调用振动器接口。

做了两年多车机Android开发,偶尔玩下手机端跨平台也蛮有意思。

然后又了解到CMP不仅仅是移动端的,还可以做web和desktop端。

在我们日常的开发过程中,对于车机设备的adb调试操作很多,一大半全是固定的流程。使用bat脚本的话又不那么灵活,体验也不好。所以我很早就想要做一个带界面的Android设备调试工具。在移动端上写纯原生的Compose界面比较熟悉了,想着这个估计也差不多的,就开启了为期一个多月的Compose for Desktop开发。开发体验可以算中上,很多的问题在stackoverflow和官网上都能找到方案。软件命名为DebugManager。

架构设计

我没有开发Desktop端的经验,不知道最优的架构设计是什么样的。使用CMP的话Google推崇的MVI模式依然可以通用,所以最初制定的技术路线就是使用响应式的架构。

由于功能单一,几乎所有操作都是执行一些命令行,获取反馈结果,所以没有抽象的很厉害,数据层直接使用单例类,使用adb工具获取数据透传到StateHolder。StateHolder为界面的状态State管理层,在Composable方法初入时,触发StateHolder的数据获取逻辑,数据拿取到之后,更新State状态,通过界面收集监听的stateflow通知composable方法刷新UI。

Google_mvi

即用户事件从上到下,数据状态从下到上,确保唯一可信数据流。

gradle配置

这一步决定DebugManager项目面向的各个平台的配置,软件版本,安装包。

由于这个软件面向不同岗位,不同操作系统,目标是一套代码适配Windows,Linux,MacOS,达到多端通用。而且目前没有交叉编译,只能在各自的系统上打包,windows打exe,ubuntu上打deb,macos上打dmg,所以我现在给使用不同系统的同事发布软件时,都是三端各打一遍。

Windows端有配置是否显示在开始菜单,桌面快捷方式,uuid用于更新识别,自行选择安装目录。

// 开始菜单
menu = true
// 桌面快捷方式
shortcut = true
// 可自行选择安装目录
dirChooser = true
// 可单独为当前用户安装,不需要管理员权限
perUserInstall = true
// 设置图标
iconFile.set(project.file("launcher/icon.ico"))
// uuid用于更新识别
upgradeUuid = "xxxx-xxxxxxx-xxxxx"

更详细的Gradle属性配置参考可以看官方github仓库的教程文档: JetBrains官方配置文档

图标配置

关于三个平台应用图标的设置,我们需要手动制作三端的图标文件。

  • Linux使用的是png格式。可以作为源文件,来制作Windows和MacOS的图标。

  • Windows端的图标为ico格式。可以通过这个在线网站来生成:

ICON图标在线制作

图片切圆角,可以使用这个网站:

在线对图片进行透明圆角处理

  • MacOS端的图标为icns格式。

注意 Mac 端的图标需要使用苹果电脑才能生成。

首先我们切到png格式的图片所在的目录,执行下面三组命令即可生成Mac端的图标文件了。第一步创建输出文件夹:

mkdir MyIcon.iconset

输入这些命令来生成不同尺寸的图标:

sips -z 16 16     original.png --out MyIcon.iconset/icon_16x16.png
sips -z 32 32     original.png --out MyIcon.iconset/icon_16x16@2x.png
sips -z 32 32     original.png --out MyIcon.iconset/icon_32x32.png
sips -z 64 64     original.png --out MyIcon.iconset/icon_32x32@2x.png
sips -z 128 128   original.png --out MyIcon.iconset/icon_128x128.png
sips -z 256 256   original.png --out MyIcon.iconset/icon_128x128@2x.png
sips -z 256 256   original.png --out MyIcon.iconset/icon_256x256.png
sips -z 512 512   original.png --out MyIcon.iconset/icon_256x256@2x.png
sips -z 512 512   original.png --out MyIcon.iconset/icon_512x512.png
sips -z 1024 1024 original.png --out MyIcon.iconset/icon_512x512@2x.png

最后合成不同尺寸的图标:

iconutil -c icns MyIcon.iconset

之后就可以看到一个后缀为icns的文件了,将其复制到项目中,gradle脚本里配置为应用图标。

图标配置过程中的bug

目前还发现一个奇怪的bug,就是有的png图标经过转换,配置到项目中,打包exe出来是正常大小,大概90M。

有的图片生成完毕之后,Windows平台打包后的 EXE 安装包大小直接暴涨到了2个G,甚至3个G,目前不确定什么原因导致的。还在排查和寻求官方的帮助。

解决

经过几轮尝试排查,问题应该出在那个windows平台的转换网站上:

https://www.butterpig.top/icopro/

通过 IDE 打开生成的ico文件,发现其实际的文件类型是JPEG,并不是显示的ico文件。

“假icon!!”

blogs_cmp_wrong_ico_file

目前怀疑图标类型错误,导致安装包暴涨。会产生这个现象的原因,可能是CMP所使用的Windows打包器的一个bug或者说一个规则吧。

使用Python的Pillow库来转换,发现生成的图标文件显示的是我需要的ICO类型了。

转换脚本很简单,如下:

from PIL import Image

def png_to_ico(png_path, ico_path):
    # 打开PNG图像
    image = Image.open(png_path)

    # 将图像转换为ICO格式
    image.save(ico_path, format='ICO', sizes=[(image.width, image.height)])

# 调用函数并传入PNG图像路径和ICO文件路径
png_to_ico('C:\\Users\\stephen\\Desktop\\logo.png', 'output.ico')

转换后的图标文件:

blogs_cmp_right_ico_file

将这个 ico 文件配置到项目之后,打包的大小已经恢复正常的90余M。

Multiplatform适配

开发Desktop跨平台碰到的的第一个问题,就是不同平台的路径连接符不一致:

在Windows上是反斜杠  \

在unix like的系统上是一个正斜杠  /

经过探索,JVM系的应用其实可以使用 File.separator 来获取这个连接符拼到路径字符串里。

而且关于平台类型的区分,Java也给我们提供了一个 System.getProperty 接口。

单例模式

在Windows平台上,多次通过入口来运行exe文件,会产生多个进程,对于本软件是没有必要的,甚至有可能导致bug。

所以需要像任务管理器那样,不管有多少次的打开动作,始终只有一个进程一个界面。

这里通过文件锁的方式来实现。

刚开启进程就创建一个文件,并将其锁定。在JVM关闭的时候,释放并删除这个文件。这样如果软件已经有一个进程在运行了,再次打开时尝试去获取这个文件的独占锁,如果获取不到,就说明已经有一个实例在运行,直接退出后打开的这个进程。

class SingleInstanceApp {

    private var lock: FileLock? = null
    private var channel: FileChannel? = null

    fun initCheckFileLock(lockFilePath: String) {
        LogUtils.printLog("initCheckFileLock")
        val file = File(lockFilePath)
        channel = RandomAccessFile(file, "rw").getChannel()
        lock = channel?.tryLock()
        if (lock == null) {
            LogUtils.printLog("Another instance is already running.", LogUtils.LogLevel.ERROR)
            exitProcess(1)
        }
        // 添加JVM关闭时的钩子,释放锁
        Runtime.getRuntime().addShutdownHook(Thread(Runnable {
            runCatching {
                lock?.let {
                    it.release()
                    channel?.close()
                    file.delete()
                }
            }.onFailure { e ->
                e.printStackTrace()
            }
        }))
    }
}

通过依赖注入到平台化的管理类中去,init方法中,当配置文件夹一创建完毕,就进行获取锁的操作。

class PlatformAdapter(private val singleInstanceApp: SingleInstanceApp) {

    init {
        println("PlatformAdapter init")
    }

    fun init() {
        createInitTempFile()
        singleInstanceApp.initCheckFileLock(lockFilePath)
    }
}

平台渠道管理

首先,定义一个枚举类来设定平台类型:

enum class PlatformType {
    UNKNOWN,
    WINDOWS,
    MAC,
    LINUX,
}

在应用初始化时,通过接口获取平台名称,解析出哪一个平台:

/**
 * 获取当前平台类型
 */
private fun getPlatformType(): PlatformType {
    val osName = System.getProperty("os.name").lowercase(Locale.getDefault())
    return when {
        osName.contains("win") -> PlatformType.WINDOWS
        osName.contains("mac") -> PlatformType.MAC
        osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> PlatformType.LINUX
        else -> PlatformType.UNKNOWN
    }
}

后面在涉及平台差分化的时候,可以使用此方法来获取,执行不同操作。

比如打开不同平台上的文件管理器:

/**
 * 打开一个文件夹
 */
fun openFolder(path: String) {
    when (getPlatformType()) {
        PlatformType.WINDOWS, PlatformType.UNKNOWN -> {
            executeTerminalCommand("explorer.exe $path")
        }

        PlatformType.MAC -> {
            executeTerminalCommand("open $path")
        }

        PlatformType.LINUX -> {
            executeTerminalCommand("xdg-open $path")
        }
    }
}

对于各个平台上执行终端命令,使用的两个方法是相同的,无需结果就直接exec(),需要执行结果就是用ProcessBuilder来执行,等待结果。

/**
 * 执行终端命令
 */
fun executeTerminalCommand(command: String) {
    runCatching {
        Runtime.getRuntime().exec(command)
    }.onFailure { e ->
        LogUtils.printLog("执行出错:${e.message}", LogUtils.LogLevel.ERROR)
    }
}

/**
 * 执行命令,获取输出
 */
suspend fun executeCommandWithResult(command: String) = withContext(Dispatchers.IO) {
    val processBuilder = ProcessBuilder(*command.split(" ").toTypedArray())
    val process = processBuilder.start()

    val reader = BufferedReader(InputStreamReader(process.inputStream))
    val output = StringBuilder()
    var line: String?
    while (reader.readLine().also { line = it } != null) {
        output.append(line).append("\n")
    }
    // 等待进程结束
    process.waitFor()
    // 关闭输入流
    reader.close()
    output.toString()
}

窗口框架

新项目的应用入口如下:

fun main() = application {

    Window(
        onCloseRequest = {

        },
        title = "DebugManager",
        undecorated = true,
        state = windowState,
        icon = painterResource("image/icon.png"),
    ) {
       ....
    }
}

我们主要的内容区就在Window这个 Composable 方法里。

通过 windowState ,我们可以设置窗口初始大小,窗口最大最小化。

undecorated 参数,这个可以配置软件界面是否选择系统默认的标题栏。由于我希望在三端上的设计语言都可以统一,都使用我自定义的标题栏,所以这里改为设置 true

有意思的一点是,在最开始将 undecorated 设为 true 后,我发现用上的Compose自定义的标题栏后,无法使用鼠标拖动窗口了,一度试了很多方案都不行。

这里最后还是咨询谷歌的 Gemini ,它给我展示了一个 WindowDraggableArea 组件,居然直接套用即可完美解决,里面的区域就是支持拖动移动的。把标题栏的 Composable 方法放在这个 WindowDraggableArea 可组合项里面,就可以鼠标拖动标题栏来移动窗口了。

WindowDraggableArea 源码方法签名如下:

@androidx.compose.runtime.Composable
@androidx.compose.runtime.ComposableInferredTarget
public fun androidx.compose.ui.window.WindowScope.WindowDraggableArea(
    modifier: androidx.compose.ui.Modifier = COMPILED_CODE,
    content: @androidx.compose.runtime.Composable () -> kotlin.Unit = COMPILED_CODE
): kotlin.Unit { /* compiled code */
}

关于各个页面之间的导航切换,我是使用的官方扩展的跨平台版本的 navigation 库。定义导航图,然后使用 navController 来切换页面。

val navController = rememberNavController()
NavHost(navController = navController, startDestination = "device_info") {
    composable("device_info") {
        DeviceInfoScreen(navController = navController)
    } 

    ...
}

每次启动应用,DebugManager 应用开屏页面,做的简单的延时跳转,timeout后自动进入主页面。

splash

功能划分

下面简单介绍下各个页面的调试功能有哪些。

在公司,一般的开发流程里有产品设计,有交互设计,UI设计,给我传达需求,输出资源。

  1. 产品的功能设计上,这个软件自己心血来潮要做。我结合日常工作中的调试痛点,还参考了 adb 的命令介绍,选取了一些热门的组合功能和单次功能,分类添加到了界面内。
  2. 在界面UI设计风格上,我是直接参考了每天打开的 Android Studio 里的主题插件, Atom One Dark 的颜色风格。

设备信息展示

一进入界面,首页当然是所连接设备的基本信息展示。

device_info

大致的实现思路如下,关于界面状态,先定义 UiState

data class DeviceState(
    val name: String? = null,
    val manufacturer: String? = null,
    val sdkVersion: String? = null,
    val systemVersion: String? = null,
    val buildType: String? = null,
    val innerName: String? = null,
    val resolution: String? = null,
    val density: String? = null,
    val cpuArch: String? = null,
    val serial:String? = null,
    val isConnected: Boolean = false
) {
    fun toUiState() =
        DeviceState(
            name = name,
            systemVersion = systemVersion,
            manufacturer = manufacturer,
            sdkVersion = sdkVersion,
            buildType = buildType,
            innerName = innerName,
            resolution = resolution,
            cpuArch = cpuArch,
            density = density,
            serial = serial,
            isConnected = isConnected
        )
}

定义好界面所需要展示的字段,再在StateHolder里维护一个StateFlow,同时对界面层暴露一个只读的字段,用于刷新界面数据。

// 单个设备信息
private val _deviceState = MutableStateFlow(DeviceState())
val deviceStateStateFlow = _deviceState.asStateFlow()

进来界面后,在协程中获取数据,界面拿到update后的数据之后自动更新信息:

   CoroutineScope(Dispatchers.IO).launch {
                prepareEnv()
                val deviceName = .....

                _deviceState.update {
                    it.copy(
                        name = deviceName,
                        manufacturer = manufacturer,
                        sdkVersion = sdkVersion,
                        systemVersion = systemVersion,
                        buildType = buildType,
                        density = displayDensity,
                        innerName = innerName,
                        resolution = displayResolution,
                        cpuArch = architecture,
                        serial = serialNum
                    )
                }
                _deviceState.value = _deviceState.value.toUiState()
                // 初始化获取文件列表
                getFileList()
            }

右侧的一堆按钮,是一些高频使用的功能。

简单的像reboot,root等,还有使用am打开隐藏app的界面,使用perfetto抓取trace,自动拉取到电脑端。

其中执行qnx命令为车机特有,现在市面上车机Android大多是运行在QNX系统上的子系统,DebugManager还可以直接桥接到QNX系统,执行更底层更精准的命令,比如执行reset重启整个IVI系统,而不只是reboot重启Android子系统。

录屏,截屏很实用,不用掏出手机到处找角度。我们提前设置好时长,通过自动执行多条指令,等操作完毕,可以直接将截屏录屏文件导出到电脑进行分享,也是我认为最好用的功能之一。

最下面还有一些基础的音量加减,模拟输入法输入等。

轮询查询机制

值得一提的是,我加入了循环获取连接设备数量和当前连接状态的机制,当电脑端的adb服务一初始化成功,就立即开启一个死循环的协程,每2s会查询一次当前设备的连接状态,设备数量。

 private fun recycleCheckConnection() {
        CoroutineScope(Dispatchers.IO).launch {
            while (true) {
                delay(2000L)
                runCatching {
                    // 通过系统命令,检索连接设备的数量是否变化
                    val deviceCount = ....
                    if (deviceCount != _deviceMapState.value.deviceMap.size) {
                        getDeviceMap()
                        MainScope().launch {
                            delay(800L)
                            getCurrentDeviceInfo()
                        }
                    }

                    // 检索当前设备连接状态
                    val result = ....
                    // 从断开到成功连接,主动刷新一次设备信息
                    if (!isConnected) {
                        getCurrentDeviceInfo()
                    }
                    isConnected = true
                    _deviceState.update {
                        it.copy(
                            isConnected = true,
                        )
                    }
                    _deviceState.value = _deviceState.value.toUiState()
                }.onFailure { error ->
                    LogUtils.printLog("${error.message}", LogUtils.LogLevel.ERROR)
                    isConnected = false
                    _deviceState.update {
                        it.copy(
                            isConnected = false,
                        )
                    }
                    _deviceState.value = _deviceState.value.toUiState()
                }
            }
        }
    }
  1. 当增减设备时,刷新设备列表,左上角展开后可以选择不同的设备进行调试。
  2. 当现在操作的设备断开连接时,会自动切换成其他设备,如果没有其他设备,就弹出警告弹窗,不允许继续操作页面了。

这两个都是轮询的。所以在重新连接设备后,会将当前状态通过state发送到界面,警告弹窗会自动消失。

软件安装管理

这个功能是耗时最长的板块之一,主要是Android系统里面每个包的信息如何展示,如何进一步对其进行替换,结合工作中积累的命令,在全网收集了很多指令,来实现软件包的管理功能。

APP列表加入了全部包扫描和三方包扫描,对于公司定制的包,也添加到了单独的筛选规则,可以自由选择查看全量信息和精简信息。

app_manage

最上面是安装功能,是使用adb install进行的操作,适合第三方app进行验证时,或者改bug进行非正式环境的验证时使用。下拉框展开后,可以选择覆盖安装,测试安装等,对应-r,-t等带参数的 install 操作。

界面展示了app的图标,版本号,包名,更新时间等。

应用图标怎么拿到的? 网上大多数的方案是说抠出apk,使用apktool解包,找到图标文件,再拿来显示。可行的确可行,但是这个速度要等到天荒地老了。

因为我之前做过一个Android端的简单的app管理应用,我选择的路线是,提前在AndroidStudio里开发一个服务app,里面设置一个Service,启动后扫描所有的已安装的app,将应用图标,应用label,包名都存到Android本地。再将这个apk内置到DebugManager安装目录的resources目录下,将其安装进系统,准备好资源后,通过adb pull拉出所需要的资源到电脑端,再读取png文件来显示到界面上。

单个app的操作

app_manage

对于选中的单个app,提供了打开应用界面,卸载,提取apk,对于系统应用,还可以push替换apk等操作。我们的测试同事在做非全量的发版验证时非常有用,不用再使用一条条繁琐的命令来替换apk升级了。

文件管理器

由于我在Android端也没有写过文件管理器应用,所以在这个页面,有些操作也是一拍脑袋想出来的,可能不算规范的解法。仍然是MVI架构,界面去监听StateHolder里面的UiState的Flow,切换目录时重新获取列表数据,update到界面来刷新UI。

file_manage

最开始的展示列表我是直接执行了”ls /”将列表发送到界面,显示根目录,解析出其中的文件文件夹,继续往子目录的话就把路径拼接起来,比如进入sdcard,就执行”ls /sdcard”,继续深入则再次拼接。同时最上方设置了返回上级,回到根目录和priv-app快捷按钮。

展示文件列表的就是@Composable LazyComuln方法。

有意思的是,我在加入item的双击和单击的区分时,最初想给Modifier定义一个扩展方法,直接实现双击回调。但是发现必须经过clickable方法来实现,这样会把外部的单机的clickable给挤掉。所以双击判断还是写在了同一个clickable里面,通过时间间隔判断的工具类来区分,单击则选中对应的文件/文件夹,双击则进入文件夹。

modifier = Modifier.clickable {
    // 点击则设置即将操作的path
    MainStateHolder.setSelectedFilePath(it.path)
    androidSelectedFile = MainStateHolder.selectedFilePath
    // 双击,执行操作
    if (DoubleClickUtils.isFastDoubleClick()) {
        if (it.isDirectory)
            destinationCall(it.path)
        else
            println("点击文件:${it.path}")
    }
}

android内的文件操作也是使用命令行的形式,cp mv rm等。

还可以将文件pull到电脑端,将电脑端的文件推送到Android端等。

命令模式

cmd

这一页比较简单,大家看到的输入框也是Compose原生的TextField方法,还自带动画,性价比蛮高。

主要实现就是将输入框的内容,拼接后直接通过Runtime.getRuntime().exec(command)执行即可。

除了最基础的adb命令透传,配合系统厂商Android端的可执行二进制程序,可以模拟车载信号的回调操作。还有语音部门的通过广播来调试的路径,整合到了DebugManager里面,一键发送广播,模拟可见扫描的点击。

关于页

about

最后就是关于页了,显示软件版本,缓存文件目录等。通过PlatformAdapter工具类获取路径,执行打开界面即可。

开源计划

这个软件最初是基于公司业务来设计开发的,有关于公司内部的信息需要抹除。 等后续有时间我会将其功能进行略微删减,改成通用性质的Android调试工具之后,会开源到Github。对CMP跨平台感兴趣的朋友,可以加关注稍作等待,后面一起进行技术交流。

12月25日已完成剥离修改开源: DebugManager开源地址

Material Design主题切换

目前进一步导入了两套主题方案,深色和浅色。

将最高级的 Composable 可组合项使用 MaterialTheme 包裹起来,初始化获取theme的值。主题值的下发设置在了 关于页面 ,操作后的存储使用跨平台版本的 DataStore 来做键值对存储。

并通过StateHolder管理器来维护这个主题状态。在切换之后,最顶级的 Composable 组合项可以立即作出反应,切换色值资源。

 MaterialTheme(
            colors = when (themeState.value) {
                ThemeState.DARK -> DarkColorScheme
                ThemeState.LIGHT -> LightColorScheme
                else -> if (isSystemInDarkTheme()) DarkColorScheme else LightColorScheme
            }
        ) {
            SplashScreen {
                XXXXXXXXXXX
            }
        }

MainStateHolder.kt

// 主题
    private val _themeState = MutableStateFlow(ThemeState.DEFAULT)
    val themeStateStateFlow = _themeState.asStateFlow()
    private val themePreferencesKey = stringPreferencesKey("ThemeState")

    /**
     * 下发主题切换,存储在dataStore中
     */
    fun setThemeState(themeState: Int) {
        CoroutineScope(Dispatchers.IO).launch {
            dataStoreHelper.dataStore.edit {
                it[themePreferencesKey] = themeState.toString()
            }
        }
        _themeState.update {
            themeState
        }
    }

    /**
     * 获取本地存储的主题
     */
    fun getThemeState() {
        CoroutineScope(Dispatchers.IO).launch {
            dataStoreHelper.dataStore.data.collect {
                val themeState = it[themePreferencesKey]?.toInt() ?: ThemeState.DARK
                LogUtils.printLog("getThemeState-> themeState:$themeState", LogUtils.LogLevel.INFO)
                _themeState.update {
                    themeState
                }
            }
        }
    }

DataStoreHelper.kt

class DataStoreHelper {

    lateinit var dataStore: DataStore<Preferences>

    fun init(path: String) {
        dataStore = createDataStore(path)
    }

    private fun createDataStore(path: String): DataStore<Preferences> {
        return PreferenceDataStoreFactory.createWithPath(
            corruptionHandler = null,
            migrations = emptyList(),
            produceFile = { path.toPath() }
        )
    }
}

Main.kt

    val themeState = mainStateHolder.themeStateStateFlow.collectAsState()

    LaunchedEffect(Unit) {
        // 获取存储的主题设置
        mainStateHolder.getThemeState()
    }

运行截图记录

开屏动画

splash

splash

设备信息

device

device

关于页

about

about

Pagination