Effective C++ - Constructors, Destructors, and Assignment Operators

前言:如何有效运用C++,包括一般性的设计策略,以及带有具体细节的特定语言特性。知道细节很重要,否则如果疏忽几乎总是导致不可预期的程序行为(undefined behavior)。本文总结对于如何使用C++的一些建议,从而让你成为一个有战斗力的C++程序员。


1 了解C++默默编写并调用了哪些函数

如果是一个空类,那么编译器可能会自动生成:
* copy构造函数
* copy assignment操作符
* 析构函数
* default构造函数

以上这些函数都是publicinline的。

class Empty {};

// 等价于
class Empty {
public:
    Empty()                       // default构造函数
    {}        
    Empty(const Empty& rhs)       // copy构造函数
    {} 
    ~Empty()                      // 析构函数(是否是virtual呢?)
    {}
    Empty& operator=(const Empty& rhs)  // copy assignment操作符
    {}
};

注意:
1. 惟有当这些函数被调用,它们才会被编译器创建出来。

Empty e1;       // default构造函数
                // 析构函数
Empty e2(e1);   // copy构造函数
e2 = e1;        // copy assignment操作符
  1. 编译器生成的析构函数是个non-virtual,除非这个class的base class自身声明有virtual析构函数。
  2. copy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。
#include<iostream>
#include<string>

template<typename T>
class NamedObject {
public:
#if 1
    NamedObject(const char* name, const T& value) :
        nameValue(name), objectValue(value)
    {
        std::cout << "NamedObject(const char* name, const T& value)\n";
    }
#endif

    NamedObject(const std::string& name, const T& value) :
        nameValue(name), objectValue(value)
    {
        std::cout << "NamedObject(const std::string& name, const T& value)\n";
    }

public:
    std::string nameValue;
    T objectValue;
};

int main()
{
    NamedObject<int> no1("gerry", 1);
    NamedObject<int> no2(no1);         // 调用copy构造函数
    NamedObject<int> no3("yang", 2);

    no3 = no1;
    std::cout << no3.nameValue << "\n";

    return 0;
}

NamedObject没有声明copy构造函数,也没有声明copy assignment操作符,所以编译器会创建这些函数当它们被调用的时候。编译器生成的copy构造函数必须以no1.nameValue和no1.objectValue为初值设定no2.nameValue和no2.objectValue。两者之中,nameValue的类型是string,而标准的string有个copy构造函数,所以no2.nameValue的初始化方式是调用stringcopy构造函数并以no1.nameValue为实参。另一个成员NameObject::objectValue的类型是int(对此template具现体而言T是int),是个内置类型,所以no2.objectValue会以拷贝no1.objectValue内的每一个bits来完成初始化。编译器为NamedObject所生成的copy assignment操作符,其行为基本上与copy构造函数一样。

请记住

编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。

2 若不想使用编译器自动生成的函数,就应该明确拒绝

Explicitly disallow the use of compiler-generated functions you do not want.

通常如果你不希望class支持某一特定机能,只要不声明对应函数就是了。但这个策略对copy构造函数copy assignment操作符却不起作用。因为,如果你不声明它们,而某些人尝试调用它们,编译器会为你声明它们。
如果你不声明copy构造函数copy assignment操作符,编译器可能会为你产出一份,于是你的class支持copying;如果你声明它们,你的class还是支持copying但这里的目标却是要阻止copying!

答案的关键是,所有编译器产出的函数都是public。为阻止这些函数被创建出来,你得自行声明它们,但这里并没有什么需求使你必须将它们声明为public。因此你可以将copy构造函数copy assignment操作符声明为private这样明确声明一个成员函数,可以阻止编译器暗自创建其专属版本;而令这些函数为private,使得成功阻止人们调用它。

一般而言,这个做法并不绝对安全。因为member函数friend函数还是可以调用你的private函数。除非你非常聪明不去定义它,那么如果有人不慎调用任何一个,会获得一个连接错误(linkage error)。

将成员函数声明为private而且故意不实现它们,这一伎俩是如此为大家接受,因而被用在C++ iostream程序库中阻止copying行为。

例子:

#include<iostream>
#include<string>

class HomeForSale;
void copy_friend(HomeForSale& lhs, HomeForSale& rhs)
{
    lhs = rhs;
}

class HomeForSale 
{
    friend void copy_friend(HomeForSale& lhs, HomeForSale& rhs);

public:
    HomeForSale() 
    {
    }
    HomeForSale(const std::string& lhs) :
        name(lhs)
    {
    }

    HomeForSale& copy_ctor(HomeForSale& lhs)
    {
        // error LNK2019: 无法解析的外部符号 "private: __thiscall HomeForSale::HomeForSale(class HomeForSale const &)" (??0HomeForSale@@AAE@ABV0@@Z),该符号在函数 "public: class HomeForSale & __thiscall HomeForSale::copy_ctor(class HomeForSale &)" (?copy_ctor@HomeForSale@@QAEAAV1@AAV1@@Z) 中被引用
        return HomeForSale(lhs);
    }

    void copy_assignment(HomeForSale& lhs)
    {
        // error LNK2019: 无法解析的外部符号 "private: class HomeForSale & __thiscall HomeForSale::operator=(class HomeForSale const &)" (??4HomeForSale@@AAEAAV0@ABV0@@Z),该符号在函数 "public: void __thiscall HomeForSale::copy(class HomeForSale &)" (?copy@HomeForSale@@QAEXAAV1@@Z) 中被引用
        *this = lhs;
    }

    std::string name;

private:
    HomeForSale(const HomeForSale&);
    HomeForSale& operator=(const HomeForSale&);
};

int main()
{
    HomeForSale h1("first");

    HomeForSale h3(h1);     // error
    HomeForSale h2 = h1;    // error

    HomeForSale h4("fouth");
    h4 = h1;                // error

    HomeForSale h5("fifth");        // linkage error
    h5.copy_ctor(h1);

    HomeForSale h6("sixth");
    h6.copy_assignment(h1);         // linkage error
    std::cout << h6.name << "\n";

    HomeForSale h7("seventh");
    copy_friend(h7, h1);              // error?

    return 0;
}

档用户企图拷贝HomeForSale对象,编译器会阻挠他。如果你不慎在member函数或friend函数之内那么做,会轮到连接器发出抱怨。

另一种方法

将连接器错误移植编译器是可能的,而且那是好事,毕竟越早发现错误越好。方法是:在一个专门为了阻止copying动作而设计的base class内,将copy构造函数copy assignment操作符声明为private

因为,只要任何人,甚至是member函数或friend函数,尝试拷贝HomeForSale对象,编译器便试着生成一个copy构造函数和一个copy assignment操作符,这些函数的“编译器生成版”会尝试调用其base class的对应兄弟,那些调用会被编译器拒绝,因为其base class的拷贝构造函数是private

这种方法也有一个问题,由于它总是扮演base class,因此使用此项技术可能导致多重继承,因为你往往还可能需要继承其他class,而多重继承有时会阻止empty base class optimization

#include<iostream>
#include<string>

class Uncopyable
{
protected:
    Uncopyable() {}      // 允许derived对象构造和析构
    ~Uncopyable() {}

private:
    Uncopyable(const Uncopyable&);             // 阻止copying
    Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale : private Uncopyable
{
public:
    HomeForSale() 
    {
    }
    HomeForSale(const std::string& lhs) :
        name(lhs)
    {
    }

    HomeForSale& copy_ctor(HomeForSale& lhs)
    {
        return HomeForSale(lhs);
    }

    void copy_assignment(HomeForSale& lhs)
    {
        *this = lhs;
    }

    std::string name;

private:
    //HomeForSale(const HomeForSale&);
    //HomeForSale& operator=(const HomeForSale&);
};

int main()
{
    HomeForSale h1("first");

    HomeForSale h3(h1);     // error
    HomeForSale h2 = h1;    // error

    HomeForSale h4("fouth");
    h4 = h1;                // error

    HomeForSale h5("fifth");        // complie err, not linkage error
    h5.copy_ctor(h1);

    HomeForSale h6("sixth");
    h6.copy_assignment(h1);         // complie err, not linkage error
    std::cout << h6.name << "\n";

    return 0;
}

请记住

为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。

3 为多态基类声明virtual析构函数

C++指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义 —— 实际执行时通常发生的是,对象的derived成分没被销毁。于是造成一个诡异的“局部销毁”对象,从而导致资源泄露。

base类没有使用virtual析构函数

#include <stdio.h>
#include <iostream>
using namespace std;

class base {
public:
    base() {cout << "base()\n";}
    ~base() {cout << "~base()\n";} // note, have no virtual

private:
    int v1;
};

class derived : public base {
public:
    derived() {cout << "derived()\n";}
    ~derived() {cout << "~derived()\n";}

private:
    int v2;
};

int main() 
{
    //derived obj;

    base *b = new derived;
    // do something
    delete b;

    return 0;
}
/*
output:
base()
derived()
~base()
 */

base类使用virtual析构函数

#include <stdio.h>
#include <iostream>
using namespace std;

class base {
public:
    base() {cout << "base()\n";}
    virtual ~base() {cout << "~base()\n";} // have virtual

private:
    int v1;
};

class derived : public base {
public:
    derived() {cout << "derived()\n";}
    ~derived() {cout << "~derived()\n";}

private:
    int v2;
};

int main() 
{
    //derived obj;

    base *b = new derived;
    // do something
    delete b;

    return 0;
}
/*
output:
base()
derived()
~derived()
~base()
 */

观点1:任何class只要带有virtual函数,都几乎确定应该也有一个virtual析构函数。

观点2:如果class不含virtual函数,通常表示它并不意图被用做一个base class。当class不企图被当做base class,令其析构函数为virtual往往是一个馊主意。因为,欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓vptr(virtual table pointer)指针指出,vptr指向一个由函数指针构成的数组,称为vtbl(virtual table)。每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl(编译器在其中寻找合适的函数指针)。这样,如果base class内含virtual函数,那么其对象的体积会增加,在32-bits计算机体系结构中将多占用32bits(vptr大小);而在64-bits计算机体系结构中多占用64bits(指针大小为8字节)。

观点3:标准库string不含任何virtual函数,但有时程序员会错误地把它当做base class。那么,当你在程序任意某处无意间将一个pointer-to-specialstring转换为一个pointer-to-string,然后将转换所得的那个string指针delete掉,则立刻被流放到”不明确行为上”。很不幸C++没有提供类似Java的final classes禁止派生的机制。

请记住

  1. 从里向外构造(ctor),从外向里析构(dtor)
  2. polymorphic (带多态性质) base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
  3. classes的设计目的如果不是作为base classes使用,或不是为了具备多态性使用,此class就不该声明virtual析构函数。

4 别让异常逃离析构函数

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。

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

class Widget {
public:
    Widget()
    {
        cout << "Widget()\n";
    }
    ~Widget()
    {
        cout << "~Widget()\n";
        throw std::runtime_error("~Widget()");
    }
private:
    int v;
};

int main()
{
    vector<Widget> w_vec;
    w_vec.resize(3);

    return 0;
}
/*
output:
Widget()
Widget()
Widget()
~Widget()
libc++abi.dylib: terminating with unexpected exception of type std::runtime_error: ~Widget()
Abort trap: 6
 */

当vector对象被销毁,它有责任销毁其内含的所有对象。假设vector内含10个对象,而在析构第一个元素期间,有个异常抛出,其他9个对象还是应该被销毁,否则它们保存的任何资源都会发生泄漏。因此,应该调用它们各个析构函数。

正确的处理方法:在析构函数里捕获每一个异常

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

class Widget {
public:
    Widget()
    {
        cout << "Widget()\n";
    }
    ~Widget()
    {
        // 析构函数里如果抛出异常,需要自己捕获处理,否则会资源泄漏
        try {
            cout << "~Widget()\n";
            throw std::runtime_error("~Widget()");
        } catch (std::runtime_error &e) {
            cout << "catch exception at ~Widget()\n";
        }
    }
private:
    int v;
};

int main()
{
    try {
        vector<Widget> w_vec;
        w_vec.resize(3);

    } catch (...) {
        cout << "catch exception at main()\n";
    }

    return 0;
}
/*
Widget()
Widget()
Widget()
~Widget()
catch exception at ~Widget()
~Widget()
catch exception at ~Widget()
~Widget()
catch exception at ~Widget()
*/

请记住

  1. 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

5 绝不在构造和析构过程中调用virtual函数

你不该在构造函数和析构函数期间调用virtual函数,因为这样的调用不会带来你预期的效果。

例如:假设你有个class继承体系,用来塑模股市交易如买进、卖出的订单等等,这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志中也需要创建一笔适当记录。

#include<stdio.h>
#include<iostream>

class Transaction { // base class
public:
    Transaction();

    // 做出一份因类型不同而不同的日志记录
    virtual void logTransaction() const = 0;
};

Transaction::Transaction()
{
    std::cout << "Transaction()\n";

    // 最后动作是记录日志
    logTransaction();
}

class BuyTransaction: public Transaction { // derived class
public:
    virtual void logTransaction() const {
        std::cout << "BuyTransaction::logTransaction()\n";
    }
};

class SellTransaction: public Transaction { // derived class
public:
    virtual void logTransaction() const {
        std::cout << "SellTransaction::logTransaction()\n";
    }
};

int main()
{
    BuyTransaction bt;

}
/*
g++ -o no_virtual_in_ctor_dtor no_virtual_in_ctor_dtor.cpp
no_virtual_in_ctor_dtor.cpp:17:2: warning: call to pure virtual member function 'logTransaction';
      overrides of 'logTransaction' in subclasses are not available in the constructor of
      'Transaction'
        logTransaction();
        ^
no_virtual_in_ctor_dtor.cpp:9:2: note: 'logTransaction' declared here
        virtual void logTransaction() const = 0;
        ^
1 warning generated.

 */

发现无法调用derived class的函数,在编译期间就报错了。把pure virtual去掉:

#include<stdio.h>
#include<iostream>

class Transaction { // base class
public:
    Transaction();

    // 做出一份因类型不同而不同的日志记录
    virtual void logTransaction() const {
        std::cout << "Transaction::logTransaction()\n";
    }
};

Transaction::Transaction()
{
    std::cout << "Transaction()\n";

    // 最后动作是记录日志
    logTransaction();
}

class BuyTransaction: public Transaction { // derived class
public:
    virtual void logTransaction() const {
        std::cout << "BuyTransaction::logTransaction()\n";
    }
};

class SellTransaction: public Transaction { // derived class
public:
    virtual void logTransaction() const {
        std::cout << "SellTransaction::logTransaction()\n";
    }
};

int main()
{
    BuyTransaction bt;

}
/*
g++ -o no_virtual_in_ctor_dtor no_virtual_in_ctor_dtor.cpp
./no_virtual_in_ctor_dtor 
Transaction()
Transaction::logTransaction()
 */

这次可以编译过了,但是发现调用的并不是派生类的virtual函数。

原因分析

  1. 在创建派生类对象时,derived class对象内的bass class成分会在derived class自身成分被构造之前先构造妥当。Transaction构造函数调用virtual函数logTransaction,这时被调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本。base class构造期间virtual函数绝不会下降到derived classes阶层,在base class构造期间,virtual函数不是virtual函数。
  2. 由于base class构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。如果此期间调用的virtual函数下降至derived classes阶层,而derived class的函数使用的local成员变量尚未初始化,将导致不明确行为。
  3. derived class对象的的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至base class,若使用运行期类型信息(runtime type information),例如dynamic_casttypeid,也会把对象视为base class类型。

相同的道理也适用于析构函数。一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++视它们仿佛不再存在,进入base class析构函数后对象就成为一个base class对象。

解决方法

如何确保每次一有Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用呢?

一种做法
是在class Transaction内将logTransaction函数改为non-virtual,然后要求derived class构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtuallogTransaction。

#include <stdio.h>
#include <iostream>
#include <string>

class Transaction { // base class
public:
    explicit Transaction(const std::string& logInfo);

    // 做出一份因类型不同而不同的日志记录
    void logTransaction(const std::string& logInfo) const {
        std::cout << "Transaction::logTransaction(): " << logInfo << "\n";
    }
};

Transaction::Transaction(const std::string& logInfo)
{
    std::cout << "Transaction()\n";

    // 最后动作是记录日志
    logTransaction(logInfo);
}

class BuyTransaction: public Transaction { // derived class
public:
    BuyTransaction(const std::string &paras)
        : Transaction(createLogString(paras)) // 将log信息传给base class构造函数
    {}
private:
    static std::string createLogString(const std::string& paras);

};

std::string BuyTransaction::createLogString(const std::string& paras)
{
    if (paras == "1") return "1+";
    else if (paras == "2") return "2+";
    else return "+";
}

int main()
{
    BuyTransaction bt1("1");
    BuyTransaction bt2("2");
}
/*
g++ -o no_virtual_in_ctor_dtor no_virtual_in_ctor_dtor.cpp
./no_virtual_in_ctor_dtor
Transaction()
Transaction::logTransaction(): 1+
Transaction()
Transaction::logTransaction(): 2+ 
 */

请记住

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class

6 令operator=返回一个reference to *this

int x, y, z;
x = y = z = 10;      // 赋值连锁形式
x = (y = (z = 10));  // 赋值采用右结合律

为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议。

#include <iostream>

class Widget {
public:
    explicit Widget(int rhs) : a(rhs)
    {
    }

    Widget& operator=(const Widget& rhs) // 返回类型是个reference,指向当前对象
    {
        a = rhs.a;
        return *this;   // 返回左侧对象
    }

    Widget& operator=(int rhs) // 此函数也适用,即使此操作符的参数类型不符协定
    {
        a = rhs;
        return *this;
    }

    Widget& operator+=(const Widget& rhs)
    {
        this->a += rhs.a;
        return *this;
    }

    void print()
    {
        std::cout << a << "\n";
    }

private:
    int a;
};

int main()
{
    Widget w(1);
    w.print();

    Widget w2(2);
    w = w2;
    w.print();

    int i = 100;
    w = 100;
    w.print();

    w += w2;
    w.print();
}
/*
 ./operator 
1
2
100
102
 */

请记住

令赋值(assignment)操作符返回一个reference to *this。

7 在operator=中处理自我赋值

自我赋值发生在对象被赋值给自己时,这看起来有点愚蠢,但是它合法。所以不要认定客户绝不会那么做。此外自我赋值动作并不总是可以一眼看出来。

// 潜在的自我赋值
a[i] = a[j];  
*px = *py;

这些并不明显的自我赋值,是名带来的结果。实际上,两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成别名,因为一个base class的reference或pointer可以指向一个derived class对象。

class Base { ... };
class Derived: public Base { ... };

// rb和*pb有可能其实是同一对象
void doSomething(const Base& rb, Derived* pd);

因此,在处理自我赋值时应该注意保证:
1. 自我赋值安全问题
2. 异常问题

class Bitmap { ... };

class Widget {
public:
    Widget& operator=(const Widget& rhs);
private:
    Bitmap* pb;
};

// 不安全的版本
Widget& Widget::operator=(const Widget& rhs)
{
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

// 安全的版本,但不具备异常安全性
// 如果new异常,Widget最终会持有一个指针指向一块被删除的Bitmap
Widget& Widget::operator=(const Widget& rhs)
{
    if (this == &rhs) return *this;   // identity test

    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

// 异常安全的版本,同时也是自我赋值安全的
// 现在如果new异常,pb保存原状
// 即使没有identity test,这段代码还是能够处理自我赋值,虽然不是最高效的方法,但是行得通
Widget& Widget::operator=(const Widget& rhs)
{
    Bitmap* pOrig = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOrig;
    return *this;
}

对于第三个版本的补充说明:
如果你很关心效率,可以把identity test再次放回函数起始处。然而这样做之前先问问自己,你估计自我赋值的发生概率有多高?因为这项测试也需要成本,它会使代码变得大一些并导入一个新的控制流分支,而两者都会降低执行速度。Prefetching, caching和pipelining等指令的效率都会因此降低。

另一个替代方案是:使用copy and swap技术。此方法,为了伶俐巧妙而牺牲了清晰性。

请记住

  1. 确保当对象自我赋值时operator=有良好行为,其中技术包括,比较来源对象和目标对象的地址,精心周到的语句顺序,以及copy-and-swap。
  2. 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

8 复制对象时勿忘其每一个成分

  1. 设计良好的OO系统会将对象的内部封装起来,只留两个函数负责对象拷贝,copy构造函数和copy assignment操作符,我们称它们为copying函数。
  2. 编译器会在必要的时候为我们的class创建copying函数,并说明这些“编译器生成版”的行为是,将被拷对象的所有成员变量都做一份拷贝。 如果你声明自己的copying函数,意思就是告诉编译器你不喜欢缺省实现中的某些行为,编译器仿佛被冒犯似的,会以一种奇怪的方式回敬你,当你的实现代码出错时却不告诉你。
  3. 如果你为class添加一个成员变量,你必须同时修改copying函数,如果你忘记了,编译器也不会告诉你。
  4. 任何时候,只要你承担起为derived class撰写copying函数的责任,必须很小心地也复制其base class成分,那些成分往往是private,所以你无法直接访问它们,你应该让derived class的copying函数调用相应的base class函数。
  5. 如果你发现你的copy构造函数和copy assginment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用,这样的函数往往是private而且常被命名为init。
// 调用base class的copy构造函数
Derived::Derived(const Derived& rhs): Base(rhs), xxx(rhs.xxx)
{
}

Derived& Derived::operator=(const Derived& rhs)
{
    Base::operator=(rhs); // 对base class成分进行赋值
    xxx = rhs.xxx;
    return *this;
}

当你编写一个copying函数,请确保:
* 复制所有local成员变量
* 调用所有base classes内的适当的copying函数

请记住

  1. copying函数应该确保复制对象内的所有成员变量,及所有base class成分。
  2. 不要尝试以某个copying函数实现另一个copying函数,应该将相近的代码放在第三个函数中,并由两个copying函数调用。
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值