11. 选择适当的操作

自由存储

单个对象的分配和释放

int* ptr = new int;  // 分配一个int类型的内存空间,并初始化为0
*ptr = 10;          // 赋值
delete ptr;        // 释放内存

数组的分配和释放

int* arr = new int[10];  // 分配一个int类型数组,包含10个元素
for (int i = 0; i < 10; ++i) {
    arr[i] = i;
}
delete[] arr;  // 释放数组内存

使用 new​ 初始化对象

std::string* str = new std::string("Hello, World!");  // 分配并初始化一个std::string对象
delete str;  // 释放内存

禁止抛出异常

int* ptr = new (std::nothrow) int;  // 使用nothrow选项,如果内存不足,不会抛出异常,而是返回nullptr
if (ptr == nullptr) {
    std::cerr << "Memory allocation failed" << std::endl;
} else {
    delete ptr;  // 释放内存
}

放置在特定空间

char* buffer = new char[sizeof(MyClass)];  // 分配内存
MyClass* obj = new (buffer) MyClass(42);  // 在已分配的内存上构造对象
// 使用obj...
obj->~MyClass();  // 显式调用析构函数
delete[] buffer;  // 释放内存

重载 new 和 delete

#include <iostream>
#include <cstdlib>

// 全局变量用于跟踪内存分配
int allocations = 0;

// 重载 new
void* operator new(std::size_t size) {
    std::cout << "Custom new called, requesting " << size << " bytes.\n";
    allocations++;
    return std::malloc(size);
}

// 重载 delete
void operator delete(void* ptr) noexcept {
    std::cout << "Custom delete called.\n";
    allocations--;
    std::free(ptr);
}

// 重载 delete
void operator delete(void* ptr, std::size_t size) noexcept {
    std::cout << "Custom delete called with size " << size << ".\n";
    allocations--;
    std::free(ptr);
}

// 重载 new[]
void* operator new[](std::size_t size) {
    std::cout << "Custom new[] called, requesting " << size << " bytes.\n";
    allocations++;
    return std::malloc(size);
}

// 重载 delete[]
void operator delete[](void* ptr) noexcept {
    std::cout << "Custom delete[] called.\n";
    allocations--;
    std::free(ptr);
}

// 重载 delete[]
void operator delete[](void* ptr, std::size_t size) noexcept {
    std::cout << "Custom delete[] called with size " << size << ".\n";
    allocations--;
    std::free(ptr);
}

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called.\n";
    }
    ~MyClass() {
        std::cout << "MyClass destructor called.\n";
    }
};

int main() {
    std::cout << "Before allocation: " << allocations << " allocations.\n";

    MyClass* obj = new MyClass();  // 调用重载的 new
    std::cout << "After single object allocation: " << allocations << " allocations.\n";

    MyClass* arr = new MyClass[5];  // 调用重载的 new[]
    std::cout << "After array allocation: " << allocations << " allocations.\n";

    delete obj;  // 调用重载的 delete
    std::cout << "After single object deletion: " << allocations << " allocations.\n";

    delete[] arr;  // 调用重载的 delete[]
    std::cout << "After array deletion: " << allocations << " allocations.\n";

    return 0;
}

重载了两个 delete 版本,这样才不会报警告,可在在线编译器尝试

列表

我们能用 {}​ 列表初始化命名变量,此外,在很多(但并非所有)地方 {}​ 列表还能作为表达式出现。它们的表现形式有两种:

  • 限定为某种类型,形如 T{......}​,意思是“创建一个 T​ 类型的对象并用 T{......}​ 初始化它”
  • 未限定的 {......}​,其类型根据上下文确定

实现模型

{} ​列表的实现模型由三部分组成:

  1. 如果 {} ​列表被用作构造函数的实参,则其实现过程与使用 () ​列表类似。除非列表的元素以传值的方式传给构造函数,否则我们不会拷贝列表的元素。

  2. 如果 {} ​列表被用于初始化一个聚合体(一个数组或者一个未提供构造函数的类)的元素,则列表的每个元素分别初始化聚合体中的一个元素。除非列表的元素以传值的方式传给聚合体元素的构造函数,否则我们不会拷贝列表的元素。

  3. 如果 {} ​列表被用于构建一个 initializer_list ​对象,则列表的每个元素分别初始化 initializer_list ​的底层数组(underlying array)的一个元素。通常情况下,我们把元素从 initializer_list ​拷贝到实际使用它们的地方。

    #include <initializer_list>
    
    class Example {
    public:
        Example(std::initializer_list<int> list) {
            for (int value : list) {
                // 处理列表中的每个元素
            }
        }
    };
    
    int main() {
        Example obj{ 1, 2, 3 };  // 使用{}列表初始化对象
        return 0;
    }
    

initializer_list​ 的底层数组是不可修改的,这意味着接受列表元素的容器必须使用拷贝操作,不能使用移动操作。

std::initializer_list<int> lst {1, 2, 3, 4, 5};
std::cout << *lst.begin() << std::endl;
*lst.begin() = 10; // 不OK,不能修改初始化列表中的元素
std::cout << *lst.begin() << std::endl;

限定列表

struct S {
	int a;
	int b;
};

S v{ 7,8 }; // 直接初始化一个变量
v = S{ 7,8 }; // 用限定列表进行赋值
S* p = new S{ 7,8 }; // 使用限定列表在自由存储上构建对象

如果某个限定列表中只含有一个元素,则其含义基本上等同于把该元素转换成另外一种类型。

未限定列表

当我们明确知道所用类型时,可以使用未限定列表。它只能被用作一条表达式,并且仅限于以下场景:

  • 函数实参
  • 返回值
  • 赋值运算符 = ​的右侧运算对象
  • 下标
#include <utility>

void g(double d) {}

struct Matrix {
    int operator[](std::pair<int, int> p) const { return 0; }
};

int f(double d, Matrix& m)
{
    int v{ 7 }; // 初始化器(直接初始化)
    int v2 = { 7 }; // 初始化器(拷贝初始化)
    int v3 = m[{2, 3}]; // 假设 m接受一个值对作为其下标

    v = { 8 }; // 赋值运算的右侧运算对象
    v += { 9 }; // 错误:不能参数与算数运算
    v = { 9 } + 10; // 错误:不能参数与算数运算
    { v } = 9; // 错误:不能作为赋值运算的左侧运算对象
    g({ 10.0 }); // 函数实
    return{ 11 }; // 返回值
}

标准库类型 initializer_list<T>​ 用于处理长度可变的 {} ​列表。我们常把它用于用户自定义容器的初始化器列表,但是除此之外也可以直接使用它。

{}​ 列表是处理同质变长列表的最简单的方法,但是注意 0 个元素的情况是个例外。此时,我们应该使用默认的构造函数。

只有当 {}​ 列表的所有元素类型相同时,我们才能推断该列表的类型。

#include<initializer_list>

int main() {
	auto x0 = {}; //错误(缺少元素类型)
	auto x1 = { 1 }; // initializer_list<int>
	auto x2 = { 1,2 }; // initializer_list<int>
	auto x3 = { 1,2,3 }; // initializer_list<int>
	auto x4 = { 1,2.0 }; //错误:元素类型不相同
}

我们无法通过推断未限定列表的类型使其作为普通模板的实参。

template<typename T>
void f(T t){}

int main() {
	f({}); //错误:初始化器的类型未知
	f({ 1 }); //错误:未限定的列表与“普通的T”不匹配
	f({ 1,2 }); //错误:未限定的列表与“普通的T”不匹配
	f({ 1,2,3 }); //错误:未限定的列表与“普通的T”不匹配
}

lambda 表达式

lambda 表达式(lambda expression)有时也称为 lambda 函数(lambda function),它是定义和使用匿名函数对象的一种简便的方式。

一条 lambda 表达式包含以下组成要件:

  • 一个可能为空的捕获列表(capture list),指明定义环境中的哪些名字能被用在 lambda 表达式内,以及这些名字的访问方式是拷贝还是引用。捕获列表位于 [] ​内。
  • 一个可选的参数列表(parameter list),指明 lambda 表达式所需的参数。参数列表位于 () ​内。
  • 一个可选的 mutable ​修饰符,指明该 lambda 表达式可能会修改它自身的状态(即,改变通过值捕获的变量的副本)。
  • 一个可选的 noexcept ​修饰符。
  • 一个可选的 -> ​形式的返回类型声明。
  • 一个表达式体(body),指明要执行的代码,表达式体位于 {} ​内。

在 lambda 的概念中,传参、返回结果以及定义表达式体等环节都与函数的相应概念是一致的。区别在于函数没有提供局部变量“捕获”的功能,这意味着 lambda 可以作为局部函数使用,而普通函数不能。

实现模型

Lambda 表达式是一种在现代编程语言中定义匿名函数的方式,它们可以被视为定义并使用函数对象的便捷方法。这种表达式允许在不显式定义函数的情况下创建函数对象,从而简化代码。

考虑一个 print_modulo ​函数,其功能是检查一个整数向量 v ​中的每个元素是否能被整数 m ​整除,如果能,则将该元素输出到输出流 os​。以下是使用 lambda 表达式实现的代码:

void print_modulo(const vector<int>& v, ostream& os, int m) {
    for_each(begin(v), end(v), [&](int x) {
        if (x % m == 0) os << x << '\n';
    });
}

在这个例子中,for_each ​算法应用于向量 v​,对每个元素执行一个 lambda 表达式。如果元素 x ​能被 m ​整除,则将其输出到 os​。

为了更好地理解 lambda 表达式的工作原理,我们可以定义一个等价的函数对象 Modulo_print​:

class Modulo_print {
    ostream& os; // 引用,用于输出
    int m;       // 值拷贝,用于整除检查

public:
    Modulo_print(ostream& s, int mm) : os(s), m(mm) {} // 构造函数

    void operator()(int x) const { // 函数调用运算符
        if (x % m == 0) os << x << '\n';
    }
};

这个类有两个成员变量 os ​和 m​,分别对应于 lambda 表达式中的捕获列表 [&os, m]​。os ​前的 & ​表示它是引用,而 m ​前的没有 & ​表示它是值拷贝。

由 lambda 表达式生成的类的对象称为闭包对象(closure object) 。如果 lambda 通过引用捕获其局部变量,则闭包对象可以优化为仅包含一个指向外层栈框架的指针。

使用 Modulo_print ​类,我们可以重写 print_modulo ​函数如下:

void print_modulo(const vector<int>& v, ostream& os, int m) {
    for_each(begin(v), end(v), Modulo_print{os, m});
}

在这个重写版本中,我们创建了一个 Modulo_print ​对象,并将其传递给 for_each ​算法,该对象将对向量 v ​中的每个元素执行相同的操作。

捕获

Lambda 表达式的第一个字符总是 [​,后面跟着的是捕获列表,它定义了 Lambda 如何捕获外部作用域中的变量。以下是几种常见的捕获列表:

  • 空捕获列表([]​):不捕获任何外部变量,Lambda 内部无法使用其外层上下文中的任何局部名字。

    sort(v.begin(), v.end(), [](int x, int y) { return abs(x) < abs(y); });
    
  • 通过引用隐式捕获([&]​):所有局部变量都通过引用访问。

    int a = 10;
    auto lambda = [&]() { cout << a; };
    
  • 通过值隐式捕获([=]​):所有局部变量都通过值访问,即捕获局部变量的副本。

    int a = 10;
    auto lambda = [&]() { cout << a; }; // a的副本被捕获
    
  • 显式捕获([捕获列表]​):通过值或引用的方式捕获指定的局部变量。

    int a = 10, b = 20;
    auto lambda = [a, &b] { cout << a << " " << b; }; // a通过值捕获,b通过引用捕获
    
  • [&, 捕获列表]​:对于未在捕获列表中出现的局部变量,通过引用隐式捕获;列表中的变量通过值捕获,列出的名字不能以 & ​为前缀。

    int a = 10, b = 20;
    auto lambda = [&, a] { cout << a << " " << b; }; // a通过值捕获,b通过引用捕获
    
  • [=, 捕获列表]​:对于未在捕获列表中出现的局部变量,通过值隐式捕获;列表中的变量通过引用捕获,列出的名字必须以 & ​为前缀。

    int a = 10, b = 20;
    auto lambda = [=, &b] { cout << a << " " << b; }; // a通过值捕获,b通过引用捕获
    

捕获的考虑因素:

  • 值捕获:适用于小对象或不需要修改外部变量的情况。
  • 引用捕获:适用于需要修改外部变量或对象较大的情况。
  • 线程安全:当 Lambda 被传递到其他线程时,通过值捕获([=]​)通常更安全,因为通过引用或指针访问其他线程的栈内存是不安全的。

Lambda 与生命周期

Lambda 表达式的生命周期可能比其调用者的生命周期更长,这可能导致潜在的问题。当 Lambda 表达式被传递给另一个线程或者被调用者存储起来以供后续使用时,这种情况尤其明显。

void setup(Menu& m) {
    Point p1, p2, p3;
    // ...计算p1, p2和p3的位置...
    m.add("draw triangle", [&]{ m.draw(p1, p2, p3); });
}

在这个例子中,setup ​函数计算三个点 p1​, p2​, p3 ​的位置,并将一个 Lambda 表达式添加到菜单 m ​中。这个 Lambda 表达式通过引用捕获了局部变量 p1​, p2​, p3​。问题在于,当用户在几分钟后点击“draw triangle”按钮时,Lambda 表达式可能会试图访问这些局部变量,而它们可能已经超出了作用域,导致未定义行为。

为了避免这种情况,我们需要确保 Lambda 表达式中使用的局部变量的生命周期至少与 Lambda 表达式一样长。这可以通过值捕获([=]​)来实现,这样 Lambda 表达式就会复制这些变量,而不是引用它们。修改后的代码如下:

void setup(Menu& m) {
    Point p1, p2, p3;
    // ...计算p1, p2和p3的位置...
    m.add("draw triangle", [=]{ m.draw(p1, p2, p3); });
}

名字空间名字

当在 Lambda 表达式中使用命名空间变量或全局变量时,由于它们总是可访问的,我们不需要在 Lambda 的捕获列表中显式捕获它们。

#include <iostream>
#include <map>
#include <utility>
#include <algorithm>

// 重载输出运算符,以便输出pair对象
template<typename U, typename V>
std::ostream& operator<<(std::ostream& os, const std::pair<U, V>& p) {
    return os << '{' << p.first << ',' << p.second << '}';
}

// 打印map对象的所有元素
void print_all(const std::map<std::string, int>& m, const std::string& label) {
    std::cout << label << ":\n{\n";
    std::for_each(m.begin(), m.end(), [](const std::pair<std::string, int>& p) {
        std::cout << p << '\n';
        });
    std::cout << "}\n";
}

int main() {
    std::map<std::string, int> m;
    m["one"] = 1;
    m["two"] = 2;
    m["three"] = 3;
    print_all(m, "Initial map");
}

lambda 与 this

在类的成员函数中使用 Lambda 表达式时,可以通过将 this ​添加到捕获列表中来访问类的成员。这种方式允许 Lambda 直接通过 this ​访问和操作类的成员,而无需复制。

然而,需注意 [this] ​和 [=] ​互不兼容,此稍有不慎就可能在多线程程序中产生竞争条件。

#include <functional>
#include <map>
#include <string>

class Request {
    using function = std::function<std::map<std::string, std::string>(const std::map<std::string, std::string>&)>; // 操作函数类型定义
    std::map<std::string, std::string> values; // 参数
    std::map<std::string, std::string> results; // 目标结果
    function oper; // 操作

public:
    Request(const std::string& s); // 解析并保存请求
    void execute() {
        // 使用this捕获列表
        [this]() { results = oper(values); }(); // 根据结果执行相应的操作
    }
};

mutable​ 的 lambda

通常情况下,人们不希望修改函数对象(闭包)的状态,因此默认设置为不可修改。换句话说,生成的函数对象的 operator()()​ 是一个 const​ 成员函数。只有在极少数情况下,如果我们确实希望修改状态(注意,不是修改通过引用捕获的变量的状态),则可以把 lambda​ 声明成 mutable​ 的。

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

void algo(std::vector<int>& v) {
    int count = v.size();
    // 使用mutable关键字允许修改闭包状态
    std::generate(v.begin(), v.end(), [count]() mutable {
        return --count;
        });
}

int main() {
    std::vector<int> vec(10);
    algo(vec);
    // 输出vec中的元素
    for (int num : vec) {
        std::cout << num << " ";
    }
    return 0;
}

每次调用 Lambda 表达式时,--count ​都会递减 count ​的值,并将这个递减后的值作为结果返回,从而生成一个递减的整数序列填充到向量中。

可以理解为,lambda 表达式是一个函数对象,这是在捕获的值是其成员变量。

默认情况下不允许修改按值传递进来的成员变量,可通过声明为 mutable​ 来绕开限制。

这里相当于修改这个对象的成员变量,并且下次在调用这个对象时,其内部成员变量是上次调用后修改的结果。

所谓的闭包对象就是指的这个函数对象,闭包就是指把变量捕获进去(封装进去)了。

调用与返回

Lambda 表达式大多数规则是从函数和类借鉴而来,有两点需要注意:

  • 参数列表的省略:如果 Lambda 表达式不接受任何参数,其参数列表可以省略。Lambda 表达式的最简形式是 []{}​。

  • 返回类型的推断:Lambda 表达式的返回类型可以由表达式本身推断得到,而普通函数则不能。

    • 如果 Lambda 的主体部分不包含 return ​语句,则返回类型为 void​。

      auto g = [&](){ f(y); };
      
    • 如果 Lambda 的主体部分有 return ​语句,且类型相同,则返回类型为该 return ​表达式的类型。

      auto z1 = [y](int x) { if (y) return x; else return 2; };
      
    • 在其他情况下,需要显式提供一个返回类型。

      auto z2 = [y]() { if (y) return 1.0; else return 2; }; // 后面推断为int,前面推断是double
      auto z3 = [y]()->int { if (y) return 1.0; else return 2; };
      

lambda 的类型

Lambda 表达式是一种特殊的函数对象,其类型被称为闭包类型(closure type)。每个 lambda 表达式都有一个唯一的类型,这意味着任意两个 lambda 表达式都不会有相同的类型。这种类型的唯一性对于模板实例化机制来说非常重要,因为它允许区分不同的 lambda 表达式。

Lambda 表达式可以作为参数传递,也可以用于初始化声明为 auto ​或者 std::function<R(AL)> ​类型的变量。这里 R ​代表 lambda 的返回类型,AL ​代表它的参数类型列表。

递归使用的 Lambda

以下是一个尝试递归反转 C 风格字符串中字符的 lambda 表达式的例子:

#include <iostream>
#include <string>
#include <functional>

void f1() { 
	// 错误:rev在推断出类型之前被使用
    auto rev = [&rev](char* b, char* e) {
        if (1 < e - b) {
            std::swap(*b, *--e);
            rev(++b, e);
        }
        };
}

void f2() {
    std::function<void(char* b, char* e)> rev = [&](char* b, char* e) {
        if (1 < e - b) {
            std::swap(*b, *--e);
            rev(++b, e);
        }
        };
    rev(&s1[0], &s1[0] + s1.size());
    rev(&s2[0], &s2[0] + s2.size());
}

由于 auto ​类型变量的类型在它被使用之前无法被推断出来,所以 f1​ 错误。

为了解决这个问题,我们需要先给 lambda 表达式一个名字,然后再使用它,如 f2​ 所示。

不捕获外部变量的 lambda

如果一个 lambda 表达式不捕获任何外部变量,我们可以将它赋值给一个指向正确类型函数的指针:

double (*p1)(double) = [](double a) { return std::sqrt(a); };
double (*p2)(double) = [&](double a) { return std::sqrt(a); }; // 错误:lambda捕获了内容
double (*p3)(double) = [](int a) { return std::sqrt(a); }; // 错误:参数类型不匹配

显式类型转换

C++ 中主要的显式类型转换方法及其特点:

  • 构造:使用花括号 {} ​提供对新值类型安全的构造。

    • 执行非窄化转换:

      • int​->double​:不允许
      • double​->int​:不允许
      • short​->int​:允许
  • 命名的转换:提供不同等级的类型转换。

    • const_cast​:对某些声明为 const ​的对象获得写入的权利。

      • 仅在 const 修饰符和 volatile 修饰符上有所区别的类型转换。
    • static_cast​:反转一个定义良好的隐式类型转换。

      • 执行关联类型之间的转换,如指针类型在同一类层次中的转换,整数到枚举类型的转换,或浮点类型到整数类型的转换。
      • 它还能执行由构造函数和转换运算符定义的类型转换。
    • reinterpret_cast​:改变位模式的含义。

      • 通常情况下它产生的新类型的值与它的实参具有相同的位模式

      • 用于非关联类型之间的转换,如整数到指针的转换。这种转换非常危险,因为它不进行类型检查,完全依赖于程序员的判断。

        IO_device* d1 = reinterpret_cast<IO_device*>(0Xff00); // 0Xff00处的设备
        
    • dynamic_cast​:动态地检查类层次关系。

      • 执行指针或引用向类层次体系的类型转换,并进行运行时检查。
    • char x = 'a';
      int* p1 = &x; // 错误:不存在char*向int*的隐式类型转换
      int* p2 = static_cast<int*>(&x); // 错误:不存在char*向int*的隐式类型转换
      int* p3 = reinterpret_cast<int*>(&x); // OK:责任自负
      struct B { /*...*/ };
      struct D : B { /*...*/ }; // D 是 B 的公有派生类
      B* pb = new D; // OK: D* 向 B* 的隐式类型转换
      D* pd = pb; // 错误:不存在 B* 到 D* 的隐式类型转换
      D* pd2 = static_cast<D*>(pb); // OK
      
      
  • C 风格的转换:提供命名的类型转换或其组合。

    • C++ 继承了 C 语言中的 C 风格类型转换,这种转换使用符号 (T)e ​来实现,其中 T ​是目标类型,e ​是表达式。C 风格类型转换可以执行 static_cast​、reinterpret_cast ​和 const_cast ​任意组合之后得到的类型转换,其目的是从表达式 e ​得到类型为 T ​的值。

      基本的 C 风格类型转换:

      int i = 10;
      double d = (double)i; // 将int转换为double
      

      指针类型转换

      void* voidPtr = &i;
      int* intPtr = (int*)voidPtr; // 将void*转换为int*
      

      移除 const 修饰符(不推荐,因为这违反了 const 的正确使用):

      const int* ci = &i;
      int* modifiablePtr = (int*)ci; // 错误地移除了const修饰符
      
    • C 风格类型转换比 C++ 的命名转换运算符更危险,因为它难以在大型程序中定位,且不易理解程序员的真实意图。

    • C 风格类型转换允许将类的指针转换为指向该类私有基类的指针,这是不安全的,应该避免这样做。

      class Base {};
      class Derived : private Base {};
      
      Derived d;
      Base* basePtr = (Base*)&d; // 错误:将Derived*转换为Base*,但Derived是私有继承自Base
      
  • 函数化符号:提供 C 风格转换的另一种形式。

    • C++ 中的函数形式的转换(function-style cast)使用 T(e) ​的语法,其中 T ​是目标类型,e ​是要转换的值。这种转换方式有时被称为函数形式的转换,它提供了一种将值 e ​转换为类型 T ​的简便方法。
    • 对于内置类型,T(e) ​等价于 C 风格的转换 (T)e​,这意味着它并不比 C 风格的转换更安全。

建议:在处理行为良好的构造时使用花括号初始化 T{v}​,而在其他情况下使用命名的转换(如 static_cast​)。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇