目录
  1. 1. 一、RAII:资源获取即初始化
    1. 1.1. 1.1 智能指针(Smart Pointers)
    2. 1.2. 1.2 Android AOSP 的 sp/wp
  2. 2. 二、虚函数与多态机制
    1. 2.1. 2.1 vtable 与 vptr 的底层原理
    2. 2.2. 2.2 纯虚函数与抽象基类
    3. 2.3. 2.3 多重继承与虚继承
  3. 3. 三、移动语义与右值引用
    1. 3.1. 3.1 Rule of Five
  4. 4. 四、模板元编程
    1. 4.1. 4.1 SFINAE(替换失败不是错误)
    2. 4.2. 4.2 CRTP(奇异递归模板模式)
    3. 4.3. 4.3 Type Traits
    4. 4.4. 4.4 变参模板(Variadic Templates)
  5. 5. 五、Lambda 表达式
  6. 6. 六、STL 容器选择指南
    1. 6.1. 6.1 vector 的扩容策略
    2. 6.2. 6.2 自定义分配器
  7. 7. 七、placement new 与手动生命周期管理
  8. 8. 八、异常安全保证
    1. 8.1. 8.1 noexcept 的作用
  9. 9. 九、线程安全与互斥锁
  10. 10. 十、C++11/14/17/20 关键特性速览
  11. 11. 十一、const 正确性
  12. 12. 十二、面试常问题目
【C/C++理论实战技术】BAT最常用的C++技术

一、RAII:资源获取即初始化

RAII(Resource Acquisition Is Initialization)是 C++ 最重要的惯用法之一。核心思想是:将资源的生命周期与对象的生命周期绑定——构造函数获取资源,析构函数释放资源。这确保了即使在异常路径下,资源也能被正确释放。

Android 的 AOSP 代码中大量使用 RAII。以 Binder 驱动的锁管理为例:

// AOSP: frameworks/native/libs/binder/IPCThreadState.cpp
// Scoped 锁的 RAII 包装
class Mutex {
public:
int lock();
void unlock();
};

class AutoMutex {
public:
AutoMutex(Mutex& m) : mLock(m) { mLock.lock(); }
~AutoMutex() { mLock.unlock(); }
private:
Mutex& mLock;
};

void IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult) {
AutoMutex _l(mProcess->mThreadCountLock); // 构造时加锁
// ... 临界区代码 ...
// 即使中间抛异常或 return,析构时自动解锁
}

源码路径:frameworks/native/libs/binder/include/binder/Mutex.h

1.1 智能指针(Smart Pointers)

C++11 标准库提供了三种智能指针,Android AOSP 还有自己的实现(sp/wp 即 StrongPointer/WeakPointer,基于轻量级引用计数)。

shared_ptr:共享所有权,引用计数为 0 时释放资源。

#include <memory>

class Connection {
public:
Connection(const std::string& addr) : m_addr(addr) {
// 建立连接
}
~Connection() {
// 关闭连接——shared_ptr 保证一定调用
}
private:
std::string m_addr;
};

// shared_ptr 的使用
void processRequest() {
auto conn = std::make_shared<Connection>("192.168.1.1:8080");
// 即使下面的函数抛异常,conn 的析构也会被调用
doSomething(conn);
}

unique_ptr:独占所有权,不可拷贝,可移动。零开销(与裸指针性能相同)。

// unique_ptr 适合作为工厂函数的返回值
std::unique_ptr<AudioTrack> createAudioTrack(int sampleRate) {
auto track = std::make_unique<AudioTrack>(sampleRate);
if (!track->init()) {
return nullptr; // 自动释放
}
return track; // 移动语义,所有权转移给调用者
}

weak_ptr:弱引用,不增加引用计数,用于打破 shared_ptr 的循环引用。

class Node {
std::string name;
std::vector<std::shared_ptr<Node>> children;
std::weak_ptr<Node> parent; // 关键:parent 用 weak_ptr,避免循环引用

public:
Node(const std::string& n) : name(n) {}
void setParent(std::shared_ptr<Node> p) { parent = p; }
};

1.2 Android AOSP 的 sp/wp

Android 的 Binder IPC 体系使用 sp<T>(强指针)和 wp<T>(弱指针)管理引用计数:

// AOSP: system/core/libutils/include/utils/RefBase.h
class RefBase {
public:
void incStrong(const void* id) const;
void decStrong(const void* id) const;
// ...
};

template<typename T>
class sp {
T* m_ptr;
public:
sp(T* other) : m_ptr(other) {
if (m_ptr) m_ptr->incStrong(this);
}
~sp() {
if (m_ptr) m_ptr->decStrong(this);
}
// ...
};

IBinderBnInterfaceBpInterface 等 Binder 核心类都继承自 RefBase,通过 sp 管理生命周期。

二、虚函数与多态机制

2.1 vtable 与 vptr 的底层原理

当一个类声明了虚函数,编译器会为这个类生成一张虚函数表(vtable),存储在只读数据段中。类的每个对象实例包含一个隐藏指针 vptr,指向该类的 vtable。

class Base {
public:
virtual void f() { printf("Base::f\n"); }
virtual void g() { printf("Base::g\n"); }
int data;
};

class Derived : public Base {
public:
void f() override { printf("Derived::f\n"); }
virtual void h() { printf("Derived::h\n"); }
};

// 内存布局:
// Base 对象: [vptr → Base_vtable, data]
// Base_vtable: [&Base::f, &Base::g]
// Derived 对象: [vptr → Derived_vtable, data]
// Derived_vtable: [&Derived::f, &Base::g, &Derived::h]

虚函数调用的开销:需要两次间接寻址(读取 vptr → 读取 vtable 中的函数指针 → 调用)。这也是为什么虚函数不能被内联优化——编译器在编译时不知道实际调用的是哪个版本。

2.2 纯虚函数与抽象基类

class IAudioTrack {
public:
virtual ~IAudioTrack() = default; // 虚析构函数是必须的!
virtual int write(const void* buffer, size_t size) = 0; // 纯虚函数
virtual int flush() = 0;
};

// Android HAL 大量使用这种模式
// frameworks/av/media/libaudioclient/include/media/AudioTrack.h

2.3 多重继承与虚继承

// 钻石问题(Diamond Problem)
class Animal {
public:
int age;
virtual void speak() = 0;
};

class Mammal : public Animal { /* ... */ };
class WingedAnimal : public Animal { /* ... */ };

// BAD: Bat 将拥有两份 Animal::age
class Bat_NonVirtual : public Mammal, public WingedAnimal { /* ... */ };

// GOOD: 虚继承,确保只有一份 Animal 子对象
class Mammal_Virtual : virtual public Animal { /* ... */ };
class WingedAnimal_Virtual : virtual public Animal { /* ... */ };
class Bat : public Mammal_Virtual, public WingedAnimal_Virtual {
public:
void speak() override { /* ... */ }
};

// 虚继承的内存开销:引入了一个额外的 vbptr(virtual base pointer)
// 指向虚基类表(vbtable),用于定位共享的基类子对象

三、移动语义与右值引用

C++11 的移动语义是一个关键优化:对于临时对象,可以将资源”移动”而不是”拷贝”,避免不必要的内存分配。

class Buffer {
char* m_data;
size_t m_size;

public:
// 移动构造函数——"窃取"资源
Buffer(Buffer&& other) noexcept
: m_data(other.m_data), m_size(other.m_size) {
other.m_data = nullptr; // 将原对象置于有效但未指定状态
other.m_size = 0;
}

// 移动赋值运算符
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] m_data; // 释放当前资源
m_data = other.m_data; // 窃取资源
m_size = other.m_size;
other.m_data = nullptr; // 使原对象安全析构
other.m_size = 0;
}
return *this;
}

// 禁用拷贝(或实现拷贝)
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
};

在 Android NDK 开发中,大 Buffer(如相机帧数据、音频数据)通过移动语义传递可以避免不必要的数据拷贝,显著提升性能。

// std::move 将左值转为右值引用,触发移动语义
std::vector<uint8_t> frame1 = captureFrame();
std::vector<uint8_t> frame2 = std::move(frame1); // frame1 的数据被移动到 frame2
// 之后 frame1 为空,但处于合法状态

std::forward 用于完美转发,保持参数的左值/右值属性:

template<typename T>
void wrapper(T&& arg) {
// std::forward<T> 保持 arg 的引用类型
// 如果传入左值 → 左值引用转发;传入右值 → 右值引用转发
target(std::forward<T>(arg));
}

3.1 Rule of Five

如果类需要自定义析构函数、拷贝构造或拷贝赋值中的任何一个,那么很可能需要全部五个(拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数):

class ResourceManager {
public:
ResourceManager(); // 默认构造
~ResourceManager(); // 析构
ResourceManager(const ResourceManager&); // 拷贝构造
ResourceManager& operator=(const ResourceManager&); // 拷贝赋值
ResourceManager(ResourceManager&&) noexcept; // 移动构造
ResourceManager& operator=(ResourceManager&&) noexcept; // 移动赋值
};

四、模板元编程

C++ 模板不仅是泛型编程的工具,还是一种编译期计算语言。Android 的 HAL(硬件抽象层)和 ART 运行时大量使用模板提升性能和复用。

4.1 SFINAE(替换失败不是错误)

SFINAE 规则是模板元编程的基础:当模板参数替换失败时,编译器不会报错,而是从重载集中排除该模板。

// 编译期判断类型是否有 foo() 方法
template<typename T>
class has_foo {
private:
typedef char yes[1];
typedef char no[2];

template<typename U, U> struct type_check;

template<typename C>
static yes& test(type_check<void (C::*)(), &C::foo>*);

template<typename>
static no& test(...);

public:
static constexpr bool value = (sizeof(test<T>(nullptr)) == sizeof(yes));
};

C++17 引入 if constexpr 后,条件编译变得更简洁:

template<typename T>
void serialize(std::vector<uint8_t>& buffer, const T& value) {
if constexpr (std::is_integral_v<T>) {
// 整数类型的序列化路径
buffer.insert(buffer.end(),
reinterpret_cast<const uint8_t*>(&value),
reinterpret_cast<const uint8_t*>(&value) + sizeof(T));
} else if constexpr (std::is_same_v<T, std::string>) {
// 字符串的序列化路径(先写长度,再写内容)
uint32_t len = value.size();
serialize(buffer, len);
buffer.insert(buffer.end(), value.begin(), value.end());
} else {
static_assert(sizeof(T) == 0, "Unsupported type for serialization");
}
}

4.2 CRTP(奇异递归模板模式)

CRTP 允许在编译期实现静态多态(无虚函数开销):

template<typename Derived>
class Singleton {
public:
static Derived& getInstance() {
static Derived instance;
return instance;
}
protected:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};

class AudioManager : public Singleton<AudioManager> {
friend class Singleton<AudioManager>;
private:
AudioManager() = default;
public:
void play() { /* ... */ }
};

// 使用: AudioManager::getInstance().play();

4.3 Type Traits

标准库的 type_traits 头文件提供了大量编译期类型判断和转换的工具:

#include <type_traits>

static_assert(std::is_same_v<int, int32_t>); // 类型相同判断
static_assert(std::is_base_of_v<Base, Derived>); // 继承关系判断
static_assert(std::is_constructible_v<MyClass, int, double>); // 可构造判断

using Decayed = std::decay_t<const int&>; // → int(去除引用和 cv 限定符)
using Underlying = std::underlying_type_t<EnumClass>; // 枚举底层类型

在 Android NDK 中的实际应用——类型安全的资源管理:

// 确保只有继承自 RefBase 的类型才能被 sp 包装
template<typename T>
class sp {
static_assert(std::is_base_of_v<RefBase, T>,
"sp<T>: T must be a subclass of RefBase");
// ...
};

4.4 变参模板(Variadic Templates)

// C++11 变参模板实现类型安全的 printf
void safe_printf(const char* format) {
// 基本情况:没有更多参数
while (*format) {
if (*format == '%') {
throw std::runtime_error("Missing argument for format specifier");
}
putchar(*format++);
}
}

template<typename T, typename... Args>
void safe_printf(const char* format, T&& value, Args&&... args) {
while (*format) {
if (*format == '%' && *(format + 1) != '%') {
std::cout << std::forward<T>(value); // 输出参数
safe_printf(format + 2, std::forward<Args>(args)...); // 递归
return;
}
putchar(*format++);
}
}

五、Lambda 表达式

C++ lambda 是 Android NDK 开发中最常用的语法糖之一,广泛应用于回调、线程任务、STL 算法等场景。

// 捕获列表语法
auto byValue = [value]() { return value * 2; }; // 拷贝捕获
auto byRef = [&value]() { value *= 2; }; // 引用捕获
auto all = [=]() { return x + y + z; }; // 拷贝捕获所有
auto allRef = [&]() { x = y = z = 0; }; // 引用捕获所有
auto mixed = [=, &result]() { result = a + b; }; // result 引用捕获,其余拷贝

// 线程中使用 lambda
std::thread task([buffer = std::move(buffer)]() {
processBuffer(buffer); // 移动捕获,转移所有权给线程
});
task.join();

// STL 算法中使用 lambda
std::vector<int> values = {5, 3, 1, 4, 2};
std::sort(values.begin(), values.end(),
[](int a, int b) { return a > b; }); // 降序排列

auto it = std::find_if(values.begin(), values.end(),
[threshold = 3](int x) { return x > threshold; });

注意:lambda 捕获引用时,必须确保 lambda 执行时引用仍然有效:

// BAD:lambda 捕获了局部变量的引用,但函数返回后 lambda 才执行
std::function<void()> createBadCallback() {
int local = 42;
return [&local]() { std::cout << local; }; // local 引用悬挂!
}

// GOOD:拷贝捕获值
std::function<void()> createGoodCallback() {
int local = 42;
return [local]() { std::cout << local; }; // 安全
}

六、STL 容器选择指南

Android NDK 开发中的容器选择直接影响性能。以下是关键容器的特性对比和选择建议:

容器 底层 随机访问 插入/删除 内存 适用场景
vector 动态数组 O(1) O(n) 连续 默认首选,尾部操作
deque 分段数组 O(1) O(1) 两端 分段 双端队列
list 双向链表 O(n) O(1) 分散,每元素额外 2 指针 大量中间插入/删除
set/map 红黑树 O(log n) O(log n) 分散 有序集合/映射
unordered_set/map 哈希表 O(1) 均摊 O(1) 均摊 分散,有桶开销 快速查找,不关心顺序
// vector 是大多数场景的最佳默认选择
// 连续内存 -> 缓存友好,比 list 的指针跳转快得多
std::vector<Pixel> scanline;
scanline.reserve(1920); // 预分配避免反复扩容

// unordered_map 适合"通过 key 快速查找 value"
std::unordered_map<std::string, std::unique_ptr<Plugin>> pluginRegistry;
auto it = pluginRegistry.find("video_decoder");

// map 适合需要有序遍历的场景
std::map<uint32_t, std::string> sortedLogs; // 按 ID 排序

6.1 vector 的扩容策略

std::vector<int> v;
v.reserve(100); // 预分配,避免多次扩容

// vector 扩容因子通常为 2(GCC)或 1.5(MSVC)
// 扩容时:分配新内存 → 拷贝/移动元素 → 释放旧内存
// 建议:已知大小时 always reserve()
// 使用 shrink_to_fit() 释放多余容量
v.shrink_to_fit(); // C++11,但只是 non-binding 请求

6.2 自定义分配器

// Android NDK 中可以为 std::vector 使用自定义分配器
// 例如使用共享内存作为底层存储
template<typename T>
class SharedMemoryAllocator {
public:
using value_type = T;

SharedMemoryAllocator(int shm_fd) : m_fd(shm_fd) {}

T* allocate(std::size_t n) {
void* ptr = mmap(nullptr, n * sizeof(T),
PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
return static_cast<T*>(ptr);
}

void deallocate(T* p, std::size_t n) {
munmap(p, n * sizeof(T));
}

private:
int m_fd;
};

std::vector<int, SharedMemoryAllocator<int>> shared_vec(
SharedMemoryAllocator<int>(shm_fd));

七、placement new 与手动生命周期管理

placement new 允许在已分配的内存上构造对象,用于对象池、内存池等场景:

#include <new>

// 对象池:预分配内存块,避免频繁 malloc/free
template<typename T, size_t N>
class ObjectPool {
alignas(T) char m_storage[sizeof(T) * N];
bool m_used[N] = {};

public:
T* allocate() {
for (size_t i = 0; i < N; i++) {
if (!m_used[i]) {
m_used[i] = true;
// placement new:在预分配内存上构造对象
return new (&m_storage[i * sizeof(T)]) T();
}
}
return nullptr; // 池已满
}

void deallocate(T* ptr) {
size_t idx = (reinterpret_cast<char*>(ptr) - m_storage) / sizeof(T);
if (idx < N && m_used[idx]) {
ptr->~T(); // 显式调用析构函数
m_used[idx] = false;
}
}

~ObjectPool() {
// 确保所有对象被析构
for (size_t i = 0; i < N; i++) {
if (m_used[i]) {
reinterpret_cast<T*>(&m_storage[i * sizeof(T)])->~T();
}
}
}
};

八、异常安全保证

C++ 标准定义了三个级别的异常安全保证:

// 1. 基本保证(Basic Guarantee)
// 异常发生时,程序状态不变,无资源泄漏,但对象状态可能已修改
void basic_guarantee_example(std::vector<int>& v, int value) {
v.push_back(value); // 可能抛 bad_alloc,但 v 仍然有效
}

// 2. 强保证(Strong Guarantee)
// 异常发生时,程序状态回滚到函数调用前(commit-or-rollback)
void strong_guarantee_example(std::vector<int>& v, int value) {
std::vector<int> tmp = v; // 先拷贝
tmp.push_back(value); // 在新拷贝上操作
v.swap(tmp); // 操作成功后再交换(swap 不抛异常)
}
// 如果 push_back 抛异常,v 保持原样

// 3. 无抛出保证(No-throw Guarantee)
// 函数承诺不抛出异常
void swap(MyClass& other) noexcept {
std::swap(m_data, other.m_data);
}

8.1 noexcept 的作用

// noexcept 不只是文档注释——它影响编译器优化和标准库行为
void mayThrow();
void neverThrow() noexcept;

static_assert(noexcept(neverThrow()));
static_assert(!noexcept(mayThrow()));

// std::vector 在扩容时,如果元素的移动构造函数是 noexcept 的,
// 则使用移动(更高效);否则回退到拷贝(保证异常安全)
class EfficientMovable {
public:
EfficientMovable(EfficientMovable&&) noexcept = default; // 关键:noexcept
};

// 使用 noexcept 运算符
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(a = std::declval<T>())) {
T tmp = a;
a = b;
b = tmp;
}

九、线程安全与互斥锁

#include <mutex>
#include <shared_mutex>

class ThreadSafeCache {
mutable std::shared_mutex m_mutex; // shared_mutex 支持读写锁
std::unordered_map<int, Data> m_cache;

public:
// 写操作——独占锁
void update(int key, Data value) {
std::unique_lock<std::shared_mutex> lock(m_mutex);
m_cache[key] = std::move(value);
}

// 读操作——共享锁(多个读操作可并发)
std::optional<Data> get(int key) const {
std::shared_lock<std::shared_mutex> lock(m_mutex);
auto it = m_cache.find(key);
if (it != m_cache.end()) {
return it->second;
}
return std::nullopt;
}
};

// scoped_lock 同时锁定多个互斥锁(避免死锁)
std::mutex mtx1, mtx2;
void safeTransfer() {
std::scoped_lock lock(mtx1, mtx2); // C++17,RAII 且防死锁
// 对两个资源进行原子操作
}

// call_once 确保只初始化一次(线程安全的单例)
class Singleton {
static std::unique_ptr<Singleton> s_instance;
static std::once_flag s_flag;

public:
static Singleton& getInstance() {
std::call_once(s_flag, []() {
s_instance.reset(new Singleton());
});
return *s_instance;
}
};

源码参考:AOSP 的 system/core/libutils/include/utils/Mutex.h 中,Android 使用 pthread_mutex_t 而非 std::mutex(因为 AOSP 代码长期基于 C++11 之前的标准)。

十、C++11/14/17/20 关键特性速览

版本 关键特性
C++11 auto, lambda, move semantics, smart pointers, constexpr, nullptr, range-for, variadic templates, static_assert, thread, chrono
C++14 generic lambda, return type deduction, decltype(auto), make_unique, binary literals
C++17 if constexpr, structured bindings, inline variables, string_view, optional/variant/any, fold expressions, std::filesystem
C++20 concepts, ranges, coroutines, modules, std::format, span, jthread, barrier/latch, semaphore

在 Android NDK 中,C++14 是默认支持的最低版本(NDK r17+),C++17 可通过 -std=c++17 启用。C++20 的部分特性在 NDK r26+ 中得到支持。

十一、const 正确性

const 是 C++ 类型系统的核心部分,不仅是”不可变”的承诺,也帮助编译器优化和防止 bug:

class AudioBuffer {
int16_t* m_data;
size_t m_size;

public:
// const 成员函数:承诺不修改对象状态
size_t size() const { return m_size; }

// 返回 const 指针:承诺不通过返回值修改数据
const int16_t* data() const { return m_data; }

// 非 const 版本:允许修改
int16_t* data() { return m_data; }

// const 引用参数:承诺不修改参数
void append(const AudioBuffer& other);
};

// const 在函数签名中区分重载
// find() 的 const 版本在 const 对象上调用时对成员加 shared_lock
// find() 的非 const 版本对成员加 unique_lock

十二、面试常问题目

Q1: shared_ptr 和 unique_ptr 的区别?什么场景用哪个?

unique_ptr 独占所有权,不能拷贝(只能移动),编译后与裸指针性能相同(零开销)。适合工厂函数返回、容器中管理对象、PIMPL 惯用法。shared_ptr 共享所有权,有引用计数开销(原子操作),适合多个所有者需要共享同一对象的场景。原则:优先用 unique_ptr,只有确实需要共享所有权时才用 shared_ptr。

Q2: 移动语义解决了什么问题?std::move 做了什么?

移动语义解决了不必要的深拷贝开销——对于包含动态分配资源的对象(如 std::vector、std::string),”移动”只需要复制指针(O(1)),而”拷贝”需要复制整个数据(O(n))。std::move 是一个无条件将左值转换为右值引用的类型转换(static_cast),它本身不移动任何东西,只是让编译器选择移动构造函数/移动赋值运算符而不是拷贝版本。

Q3: std::mutex 和 std::shared_mutex 的区别?

std::mutex 是排他锁(互斥锁),同一时刻只有一个线程能获取锁。std::shared_mutex 支持两种锁定模式:shared_lock(读锁,允许并发读)和 unique_lock(写锁,独占)。在读多写少的场景(如缓存访问),shared_mutex 可显著提高并发性能。Android 中 android:hardware:details:utils 命名空间下有类似实现。

Q4: 为什么在 Android NDK 中 vector 通常优于 list?

虽然 list 的理论插入/删除复杂度是 O(1)(vs vector 的 O(n)),但现代 CPU 的缓存架构使得连续内存的访问远快于指针跳转。vector 在连续内存中存储元素,CPU 预取(prefetch)可以高效工作;list 的每个节点分散在堆上,每次跳转都可能造成 cache miss。除非元素非常大(如 > 1KB)且频繁在中间插入/删除,否则 vector 通常更快。

Q5: 什么是 vtable?虚函数调用的开销有多大?

vtable 是编译器为每个包含虚函数的类生成的一张函数指针表(存储在只读数据段)。每个对象包含一个隐藏的 vptr 指针(8 字节在 64 位系统上)指向该类的 vtable。虚函数调用需要:(1) 通过对象获取 vptr;(2) 在 vtable 中查找函数指针;(3) 通过函数指针调用。这比普通函数调用多了两次内存访问(两次间接寻址),约 5-20 个 CPU 周期。更关键的性能影响是:虚函数阻止了编译器内联优化,因为编译时无法确定实际被调用的函数版本。


参考源码路径:

  • AOSP RefBase:system/core/libutils/include/utils/RefBase.h
  • AOSP StrongPointer:system/core/libutils/include/utils/StrongPointer.h
  • AOSP Mutex:system/core/libutils/include/utils/Mutex.h
  • C++ Standard Library:https://en.cppreference.com/w/
  • Android NDK 文档:https://developer.android.com/ndk/guides/cpp-support
打赏
  • 微信
  • 支付宝

评论