C/C++语言

本文围绕 C/C++ 展开,介绍了 const、static、extern 等关键字的作用,阐述了指针和引用的区别及作用,说明了避免野指针的方法,对比了 malloc、free 和 new、delete 等的差异,还讲解了类型转换、构造函数、深拷贝浅拷贝、继承下构造与析构顺序以及动态库和静态库的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

const 关键字的作用

  • 作用:
    • 避免修改。
    • 避免多次分配内存。
      • #define 会在编译期进行替换,多次分配内存。
        #define var1 100
        int a, b;
        a = var1; // 分配内存
        b = var1; // 分配内存
        
        const int var2 = 100; // 分配内存
        int c, d;
        c = var2;
        d = var2;
        
    • 类型检查、作用域检查。
  • 定义变量
    • C 语言
      • 局部 const:被分配在栈区,可以通过指针进行修改。
      • 全局 const:被分配在静态全局区,不能通过指针修改值,修改的话会报段错误。
    • C++
      • 局部 const:被加载到符号表中,通过指针修改无效。
      • 全局 const:被分配在静态全局区,不能通过指针修改值,修改的话会报段错误。
    • 指针
      // const 离谁近,就修饰谁
      // 指针常量: ptr1 的指向不能被修改,但可以通过指针来修改它所指向的内容
      char* const ptr1;
      
      // 常量指针: ptr2 指向的内存区域不能被修改
      // 不能通过指针来修改它指向的内容,可以通过原来的声明修改
      const char* ptr2; 
      
  • 修饰函数参数
    • 让函数内部不可以修改函数参数。
      void func(const T &a) {
      
      }
      
  • 修饰函数返回值
    • 运算符重载中使用,防止修改临时对象的值。
      class A {
      public:
      	const A operator + (const A &a) {
      
      	}
      }
      
  • 类中常成员函数
    • 常成员函数只能调用常成员变量和常成员函数。常对象只能访问常成员函数。
    • 普通成员函数可以调用常成员函数。
    • 作用:
      • 避免修改普通成员变量。
      • 用于函数重载。
      class B {
      	void func() const {
      
      	}
      	void func() {
      	
      	}	
      }
      B b1;
      b1.func();
      const B b2;
      b2.func();
      

static 关键字的作用

  • 修饰的对象
    • 变量
    • 函数
  • 使用场景
    • 函数体中:使用 static 修饰变量时,该变量的内存只会被分配一次,同时该变量的访问范围只在该函数体内。
    • 模块中:
      • 在源文件(.c / .cpp)中使用 static 修饰变量或函数,该变量或函数只能在该源文件中被访问。
      • 在头文件(.h / .hpp)中使用 static 修饰变量或函数,所有包含该头文件的源文件都可以访问该变量或函数。
      • 主要作用是避免命名重复,因为在多个模块中使用 static 修饰相同名称的变量,不会发生冲突。
    • 类中
      • 使用 static 修饰的成员变量或成员函数,其属于整个类。
      • 静态成员函数不接收 this 指针,只能访问静态成员变量。

指针和引用的作用以及区别

  • 区别
    • 是否需要初始化
      • 指针不需要初始化,但是最好初始化,避免野指针。
      • 引用必须要初始化,并且不能初始化为空对象,初始化后不能被改变。
    • 是否允许为空
      • 指针可以为空。
      • 引用不可以为空。
    • 是否直接操作对象
      • 指针通过某个指针变量指向一个对象,对它所指向的变量间接操作
      • 引用是目标对象的别名,对引用操作就是直接对目标对象操作
    • 是否是对象
      • 指针是对象,指针是有地址的,可以定义指针的指针。
      • 引用不是对象,没有实际地址,不能定义引用的指针,也不能定义引用的引用。
  • 作用
    • 引用的作用:
      • 传参时避免内存分配、对象数据的复制。
      • 作为函数返回值,避免对象数据的复制。
    • 指针的作用
      • 传参时仅仅分配指针的内存空间,避免对象数据的复制。
      • 实现多态的方法之一(基类指针指向子类具体对象)。

如何避免野指针

  • 什么是野指针,使用后有什么问题?
    • 如果某指针没有初始化,那么系统会为其随机分配一个地址。
    • 如果使用野指针,容易造成内存泄漏,出现段错误。
  • 如何避免?
    • 指针没有指向具体对象时,要初始化为空。
    • 使用指针操作指针指向的对象时,要检查是否为指针对象分配内存。
    • 使用 newmalloc 分配空间后,需要检查指针是否为空,如果为空,说明分配内存失败,这时需要强制退出。
    • 在 C 语言中,使用 malloc 之后,需要将空间数据置空(memset)。
    • 释放内存后,需要将指针置空。

malloc、free 和 new、delete 的区别

  • 定义
    • mallocfree 是 C 语言中的库函数,不可以被重载。
    • newdelete 是 C++ 中的操作符,可以被重载。
  • 使用方式
    • new 会自动计算所需分配的内存;malloc 需要手动计算所需分配的内存。
    • new 返回的是对象类型的指针;malloc 返回的是 void*,使用时需要转换为所需的类型。
    • delete 释放内存时需要对象类型的指针;free 释放内存时需要 void* 类型的指针。
    • new 分配失败会抛出异常;malloc 分配失败会返回 null
    • new 在自由存储区(free store)上分配内存;malloc 在堆上分配内存。
      • free store 是一个 C++ 抽象的概念:new 是一个操作符,在重载操作符的过程中,内部可以用 malloc 来分配内存,也可以通过内存池在静态全局区分配内存,所以 free store 既可以在堆上分配内存,也可以在静态全局区分配内存。
    • new 先调用 operator new 函数,申请足够的内存;然后调用构造函数来初始化成员变量;最后返回该对象类型的指针。
    • delete 先调用析构函数,再调用 operator delete 函数释放内存。
    • mallocfree 则是直接申请内存,释放内存。
    • deletefree 调用后,内存不会立刻被释放,指针也不会指向空,为了避免野指针,释放内存后(只是告诉操作系统:我们不使用这块内存了,你可以给其它地方使用),应该把指针指向空。
#include <iostream>
#include <new> // 包含新和删除的操作符

// 定义一个简单的内存池类
class SimpleMemoryPool {
public:
    static SimpleMemoryPool& getInstance() {
        static SimpleMemoryPool instance; // 局部静态对象作为单例
        return instance;
    }

    void* allocate(std::size_t size) {
        if (freeList.empty()) {
            // 如果没有空闲内存块,这里应该实现扩展内存池的逻辑
            // 为了简化,我们只打印一条消息
            std::cerr << "Memory pool exhausted." << std::endl;
            return nullptr;
        }
        // 从空闲列表中取出一个内存块
        void* ptr = freeList.top();
        freeList.pop();
        return ptr;
    }

    void deallocate(void* ptr) {
        freeList.push(reinterpret_cast<char*>(ptr));
    }

private:
    std::stack<char*> freeList; // 用于存储空闲内存块的栈
    SimpleMemoryPool() { // 私有构造函数,确保单例
        // 初始化内存池,这里只是示例,实际应用中需要更复杂的逻辑
        for (int i = 0; i < 10; ++i) {
            freeList.push(new char[1024]); // 分配1024字节的内存块
        }
    }

    ~SimpleMemoryPool() {
        // 清理内存池,释放所有内存块
        while (!freeList.empty()) {
            delete[] freeList.top();
            freeList.pop();
        }
    }
};

// 重载 operator new,使用 SimpleMemoryPool 分配内存
void* operator new(std::size_t size) {
    void* ptr = SimpleMemoryPool::getInstance().allocate(size);
    if (ptr) {
        std::cout << "Custom operator new called. Allocated " << size << " bytes." << std::endl;
    } else {
        std::cerr << "Failed to allocate memory." << std::endl;
        throw std::bad_alloc();
    }
    return ptr;
}

// 重载 operator delete,使用 SimpleMemoryPool 释放内存
void operator delete(void* ptr) noexcept {
    std::cout << "Custom operator delete called." << std::endl;
    SimpleMemoryPool::getInstance().deallocate(ptr);
}

int main() {
    int* p = new int; // 使用自定义的 operator new 分配内存
    delete p; // 使用自定义的 operator delete 释放内存
    return 0;
}

extern 关键字作用

  • 原理:引用还没有声明的变量或者函数,这个变量或者函数在其它地方声明了(该变量或函数不能是静态的)。
  • 使用场景:
    • C++ 调用 C 语言编译的变量或者函数。
      • C++ 支持函数重载,在编译过程中会对变量或者函数进行重命名 — 在变量名或者函数名前加一些特殊字符,通过这种方式支持重载;而 C 语言重命名比较简单。
      • 因为 C++ 和 C 语言在编译过程中对变量或者函数的命名方式不一样,所以需要告诉编译器我们现在要访问的全局变量以及函数是以 C 语言进行编译的,那么就需要使用 extern "C"
      gcc extern_c.c -c # 生成 extern_c.o
      gcc extern_cpp.cpp -c # 生成 extern_cpp.o
      gcc extern_c.o extern_cpp.o -o extern_test
      
      // extern_c.c
      #include <stdio.h>
      
      int var_c = 99;
      void func_c() {
      	printf("func_c var_c = %d\n", var_c);
      }
      
      // extern_cpp.cpp
      #include "stdio.h"
      extern "C" int var_c;
      extern "C" void func_c();
      
      int main() {
      	func_c();
      	var_c = 100;
      	printf("extern_cpp var_c = %d \n", var_c);
      }
      
    • 都是 C 语言或者都是 C++。
      • 使用文件内部还没有声明的变量或者函数。
        #include <iostream>
        
        int main() {
        	extern int var_main;
        	extern void func_main();
        	
        	func_main();
        	std::cout << "main var_main = " << var_main << std::endl;
        	
        	return 0;
        }
        int var_main = 100;
        void func_main() {
        	std::cout << "func_main var_main = " << var_main << std::endl; 
        }
        
      • 使用外部文件声明的变量或者函数。
        g++ extern_main.cpp extern_other.cpp -o extern_test
        
        // extern_other.cpp
        #include <iostream>
        
        int var_other = 99;
        
        void func_other() {
        	std::cout << "func_other var_other = " << var_other << std::endl; 
        }
        
        
        // extern_main.cpp
        #include <iostream>
        
        int main() {
        	extern int var_other;
        	extern void func_other();
        	
        	func_other();
        	std::cout << "main var_main = " << var_other << std::endl;
        	
        	return 0;
        }
        

strcpy、sprintf、memcpy 的区别

  • 相同点:都可以实现字符串拷贝(开发会使用 strcpy 进行字符串拷贝)。
  • 不同点:
    • 实现功能
      • strcpy 实现字符串拷贝,遇到 \0 结束(会把 \0 拷贝过去)。
      • sprintf 主要用来格式化字符串。
      • memcpy 实现内存块拷贝,根据 size 大小进行复制。
      // sprintf stdio.h
      // strcpy、memcpy string.h
      #include <stdio.h>
      #include <string.h>
      
      int main() {
      	char buf_a[64] = {'z', 'z', 'z', 'z', 'z'};
      	char buf_b[64] = {'a', 'b', 'c', '\0', 'd'};
      	
      	char * str = strcpy(buf_a, buf_b); 	// int n = sprintf(buf_a, "%s", buf_b); 
      	printf("str = %s, buf_a = %s \n", str, buf_a);
      	
      
      	int n = sprintf(buf_a, "len: %d, str: %s, addr:%p", 6, buf_b, buf_b);
      	printf("n = %d, buf_a = %s \n", n, buf_a);
      	
      	char buf_c[64] = {'z', 'z', 'z', 'z', 'z'};
      	char buf_d[64] = {'a', 'b', 'c', '\0', 'd'};
      	
      	memcpy(buf_c, buf_d, 3);
      	printf("buf_a : %s \n", buf_c);
      	memcpy(buf_c, buf_d, 4);
      	printf("buf_a : %s \n", buf_c);	
      		
      	return 0;
      }
      
    • 执行效率:
      • memcpy 最快,strcpy 次之(因为要检查 \0),sprintf 最慢(因为要解析格式化字符串,还可能要进行数据类型转换)。
    • 操作对象
      • strcpy 操作对象为字符串。
      • sprintf 操作对象可以为多种数据类型。
      • memcpy 操作的是内存地址。
    • 头文件
      • sprintf 头文件为 stdio.h
      • strcpymemcpy 头文件为 string.h

C/C++ 中类型转换以及使用场景

  • C 语言中的类型转换:
    • (T) exp
    • T(exp)
    • 弊端:如果转换出错,未来复现 bug 的时候,很难找到类型转换的位置。
  • C++ 中的类型转换:
    • static_cast<T>(exp)(编译期行为)
      • 类层次间转换。
        • 上行转换是安全的。(子类指针转换为父类指针)
        • 下行转换是不安全的,没有动态类型检查。(父类指针转换为子类指针)
      • 基本类型转换
      • 空指针转换为目标类型的空指针。
      • non-const 转换为 const(反之不行)。
      • 局限性:不能去掉 constvolitale 等属性。
    • const_cast<T>(exp)(编译期行为)
      • 去掉对象指针或对象引用的 const 属性
      • 目的:修改指针或引用的权限,可以通过指针或引用修改某块内存的值。
    • dynamic_cast<T>(exp)(运行时行为)
      • 用于多态,在运行时进行类型转换,有动态类型检查,是安全的
      • 在一个类层次结构中安全地类型转换,把基类指针或引用转换为派生类指针或引用。(下行转换)
      • 因为不存在空引用,所以转换失败会抛出 bad_cast 异常。
      • 因为存在空指针,所以转换失败会返回 0
    • reinterpret_cast<T>(exp)(编译期行为)
      • 改变指针或引用的类型
      • 将指针或引用转换为一个整型。
      • 将整型转换为指针或引用。
      • T 必须为指针、引用、整型、函数指针、成员指针(this 指针)。
      • 仅仅是比特位的拷贝,没有安全检查
  • 使用场景:
    • 基本类型转换用 static_cast
    • 去掉 const 属性用 const_cast
    • 多态类之间的类型转换用 dynamic_cast
    • 不同类型的指针类型转换使用 reinterpret_cast

构造函数有哪些,以及调用时机

#include <iostream>

using namespace std;

class T {
public:
    T() { 
        cout << "构造函数 T(): " << this << endl;
    }
    ~T() {
        cout << "析构函数 ~T(): " << this << endl;
    }
    
    T(const T&) {
        cout << "T(const T&) 拷贝构造: " << this << endl;
    }

    T& operator=(const T&) {
        cout << "T& operator=(const T&) 拷贝赋值构造: " << this << endl;
    }

    T(T &&) {
        cout << "T(T &&) 移动构造: " << this << endl;
    }
    
    T& operator=(T &&) {
        cout << "T& operator=(T &&) 移动赋值构造: " << this << endl;
    }
    
};

T CreateT() {
    T temp;
    return temp;
}

int main() {
    { // 拷贝构造
        T t1; 
        T t2 = t1;
        T t3(t1);
        T t4{t1};
    }
    { // 拷贝赋值构造 
        T t1;
        T t2;
        t1 = t2;
    }
    { // 移动构造
        cout << "=======" << endl;
        T t = CreateT(); 

        T t1;
        T t2(std::move(t1));
        cout << "=======" << endl;

        /*
            构造函数 T(): 0x7ffe283f9315

            构造函数 T(): 0x7ffe283f9316
            T(T &&) 移动构造: 0x7ffe283f9317
        */

        /* 禁掉返回值优化:-fno-elide-constructors
            1. 先看类有没有移动构造
            2. 然后看类有没有拷贝构造
            3. 都没有,则报错
            
            构造函数 T(): 0x7ffff8442f27
            T(T &&) 移动构造: 0x7ffff8442f57
            析构函数 ~T(): 0x7ffff8442f27
            T(T &&) 移动构造: 0x7ffff8442f55
            析构函数 ~T(): 0x7ffff8442f57

            构造函数 T(): 0x7ffff8442f56
            T(T &&) 移动构造: 0x7ffff8442f57
        */
    }
    { // 移动赋值构造
        T t;
        t = T();

        T t1, t2;
        t1 = std::move(t2);
    }

    return 0;
}

C++ 什么时候生成默认构造函数

  • 空的类定义,不会生成默认构造函数,没有意义。即便里面包含成员变量,如果成员变量是基础类型,还是不会生成默认构造函数,因为编译器也不知道用什么值去初始化。
  • 什么情况下会生成默认构造函数?
    • A 内数据成员是对象 B,而类 B 提供了默认构造函数。
      • 为了让 B 的构造函数能被调用到,不得不为 A 生成默认构造函数。
    • 类的基类提供了默认构造函数。
      • 子类构造函数要先初始化父类,再初始化自身成员变量。
      • 如果父类没有提供默认构造函数,子类也无需提供默认构造函数。
      • 如果父类提供了默认构造函数,子类不得不生成默认构造函数。
    • 类内定义了虚函数。
      • 为了实现多态机制,需要为类维护一个虚函数表。
      • 类的所有对象都需要保存一个指向该虚函数表的指针。
      • 因为对象需要初始化指向该虚函数表的指针,所以不得不提供默认构造函数来初始化虚函数表指针。
    • 类使用了虚继承。
      • 虚基类表记录了虚基类与本类的偏移地址。
      • 为了实现虚继承,对象在初始化阶段需要维护一个指向虚基类表的指针。
      • 因为对象需要初始化指向该虚基类表的指针,所以不得不提供默认构造函数来初始化虚基类表指针。

C++ 什么时候生成默认拷贝构造函数

  • 如果不提供默认拷贝构造函数,那么会按照位拷贝(一个字节一个字节地拷贝)进行拷贝,有些时候位拷贝会出现不是我们所预期的行为。
  • A a = b;
  • 什么时候必须生成默认拷贝构造函数?
    • 类成员变量也是一个类,该成员类有默认拷贝构造函数。
      • 为了让成员类的默认拷贝构造函数能够被调用到,不得不为类生成默认的拷贝构造函数。
    • 类继承自一个基类,这个基类有默认拷贝构造函数。
      • 子类执行拷贝构造函数的时候,先调用父类的拷贝构造函数。
      • 为了能调用到父类的拷贝构造函数,子类不得不生成默认的拷贝构造函数。
    • 类成员中包含一个或多个虚函数。
      • 为了实现多态机制,需要为类维护一个虚函数表。
      • 类所有对象都需要保存一个指向该虚函数表的指针。
      • 如果不提供默认拷贝构造函数,会进行一个位拷贝,那么虚函数表指针可能会丢失。
        • 本类的虚函数表指针被覆盖掉
        • 如果提供了拷贝构造函数,编译器会为我们初始化虚函数表指针
      • 所以不得不为类生成默认拷贝构造函数,完成虚函数表指针的拷贝。
    • 类继承自一个基类,这个基类有一个或者多个虚函数。
      • 如果不提供默认构造函数,会进行位拷贝,那么基类拷贝构造函数就不能被调用,从而虚函数表指针可能会丢失。
      • 所以不得不为类生成默认拷贝构造函数,以此完成基类拷贝构造函数的调用,从而完成虚函数表指针的拷贝。

C++ 什么是深拷贝、浅拷贝

  • 拷贝情况:
    • 用同类的对象构建一个新的类对象。
      A a1;
      A a2(a1);
      
    • 函数传参为类对象,值传递,类的复制。
    • 函数返回值是类对象。
  • 运算
    • =
    • 拷贝构造函数
  • 浅拷贝
    • 对象中的成员数据的简单赋值。
  • 深拷贝
    • 为对象中的动态成员(指针)重新动态分配空间,或者重新分配资源。
    • 重写拷贝构造函数,重载 = 运算符。

继承下的构造函数和析构函数执行顺序

  • 继承下构造函数按照依赖链从上往下进行构造析构函数按照依赖链从下往上进行析构
  • 单继承:
    • 成员类按照顺序构造按照相反顺序析构
    • 类的构造依赖成员类的构造基类比成员类依赖性更强
      class Base1 {
      public:
          Base1() {
              cout << "Base1 construction" << endl;
          }
      
          ~Base1() {
              cout << "~Base1 destruction" << endl;
          }
      };
      
      class Base2 {
      public:
          Base2() {
              cout << "Base2 construction" << endl;
          }
      
          ~Base2() {
              cout << "~Base2 destruction" << endl;
          }
      
      private:
          Base1 b1;
      };
      
      class Drive1 {
      public:
          Drive1() {
              cout << "Drive1 construction" << endl; 
          }
      
          ~Drive1() {
              cout << "~Drive1 destruction" << endl;
          }
      };
      
      class Drive2 : public Base2 {
      public:
          Drive2() {
              cout << "Drive2 construction" << endl; 
          }
      
          ~Drive2() {
              cout << "~Drive2 destruction" << endl;
          }
      private:
          Drive1 d1;
      };
      
      
      int main () {
          /*
            Base1 construction
            Base2 construction
            Drive1 construction
            Drive2 construction
            ~Drive2 destruction
            ~Drive1 destruction
            ~Base2 destruction
            ~Base1 destruction
          */
          Drive2 d;
      
      
          return 0;
      }
      
  • 多继承。
    • 成员类按照顺序构造按照相反顺序析构
    • 类的构造依赖成员类的构造基类比成员类依赖性更强
    • 多继承中基类按照声明顺序构造按照相反顺序析构
      class MBase1 {
      public:
          MBase1() {
              cout << "MBase1 construction" << endl;
          }
      
          ~MBase1() {
              cout << "~MBase1 destruction" << endl;
          }
      };
      
      class MBase2 {
      public:
          MBase2() {
              cout << "MBase2 construction" << endl;
          }
      
          ~MBase2() {
              cout << "~MBase2 destruction" << endl;
          }
      };
      
      class MDrive1 {
      public:
          MDrive1() {
              cout << "MDrive1 construction" << endl; 
          }
      
          ~MDrive1() {
              cout << "~MDrive1 destruction" << endl;
          }
      };
      
      class MDrive2 {
      public:
          MDrive2() {
              cout << "MDrive2 construction" << endl; 
          }
      
          ~MDrive2() {
              cout << "~MDrive2 destruction" << endl;
          }
      };
      
      class MDrive : public MBase1, public MBase2 {
      public:
          MDrive() {
              cout << "MDrive construction" << endl; 
          }
      
          ~MDrive() {
              cout << "~MDrive destruction" << endl;
          }
      
      private:
          MDrive1 md1;
          MDrive2 md2;
      };
      
      
      int main () {
          /*
            MBase1 construction
            MBase2 construction
            MDrive1 construction
            MDrive2 construction
            MDrive construction
            ~MDrive destruction
            ~MDrive2 destruction
            ~MDrive1 destruction
            ~MBase2 destruction
            ~MBase1 destruction
          */
          MDrive md;
      
      
          return 0;
      }
      

动态库和静态库的区别

  • 生成方式:
    • 静态库生成。
    • 动态库生成。
  • 链接方式:
    • 静态链接:把静态库编译进目标文件。
    • 动态链接:
      • 没有把库编译进目标文件。
      • 程序运行时才去加载运行代码。
        • 地址无关代码技术 -fPIC
        • 装载时重定位,定位到 LD_LIBRARY_PATH
      • 链接时只会做语法检查。
  • 空间占用:
    • 如果多个应用程序都使用到了同一个静态库, 那么该静态库会存在多个副本,内存和磁盘都有多份。
    • 动态库则只有一个副本。
  • 使用方式:
    • 静态库所在程序可以直接运行。
    • 动态库所在程序需要动态加载,程序的运行环境需要指定查找路径 LD_LIBRARY_PATH
  • 执行速度:
    • 静态库快。
    • 动态库慢。
  • 库文件发生变更:
    • 接口改变:静态库和可执行程序、动态库和可执行程序都需要重新编译。
    • 接口实现改变:静态库和可执行程序需要重新编译;仅仅是动态库需要重新编译。
// g++ -c add.cpp -o add.o
// g++ -c del.cpp -o del.o
// 静态库生成:ar rcs libapi.a del.o add.o
// 动态库生成:g++ -shared -fPIC -o libapi.so del.o add.o

// 使用静态库: g++ -static main.cpp -o main -L./ -lapi -I./
// 使用动态库: 
//   1. export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/holo_render/test
//   2. g++ main.cpp -o main -L./ -lapi -I./
 
int main() {
    cout << add(1, 2) << endl;
    cout << del(1, 2) << endl;

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值