C++面试总结

C语言和C++的区别:
区别1:C语言是面向过程的语言; 主要模式 输入 + 处理过程 + 输出。
C++是面向对象的语言,主要特征:封装、继承和多态,封装实现的细节,代码模块化;继承:派生类继承父类的数据和方法,扩展已经存在的模块,实现代码重用;多态:一个接口有多种实现方式,派生类重写父类虚函数,实现接口重用(多态)。

区别2:C /C++动态内存管理方法不一样,C是malloc/free,C++除此之外还有new/delete。
区别3:C++有引用,C中不存在这个概念。C++新版本一直在迭代新特性。
区别4:C++是C的超集,兼容C 包含C的内容,并且提供大量C++特有的新特性。

NULL和nullptr:
C语言中,NULL是 (void*)0
C++中,NULL是0; C++不能将void*类型隐式转换为其它指针类型。 用0来表示空指针。

nullptr:(1)可以标识空指针,(2)可以转换为任何指针类型和布尔类型。

二进制程序文件启动过程,如何分配内存?
1.会将二进制依赖的所有dll(二进制本身自带的dll,或者依赖的系统dll),都加载到进程空间中,这些dll本身自己也是二进制可执行文件,都加载到进程的代码段内存区。
2.等待所有依赖的dll加载完毕到进程空间后,才会将二进制主程序加载到进程空间,
3.然后启动运行时库
4.给全局变量,并执行初始化操作,分配全局内存区。
5.这时候才进入main函数,程序才算真正启动起来。
6.进入函数时,从所在线程的栈内存中,给函数局部变量分配栈内存; 遇到malloc/new,在堆上分配堆内存。

lambda: (可以看做未命名的内联函数)
(思想来源于函数式编程
表达式,用于定义并创建匿名函数对象,简化编程工作。
lambda表达式 比起普通函数 的 区别: 没有函数名,(匿名函数和具名函数(普通函数)的区别)
lambda表达式的基本语法结构如下:
[capture list] (parameter list) specifiers exception -> type (function body)
[capture list]: 捕获列表,[]这个方括号必须要有;lambda函数最开始的位置,编译器根据[]来判断接下来的代码是否是lambda函数,去捕获上下文变量,给lambda使用;

捕获方式有3种:
(1) 值捕获: 外部变量以const引用方式传递到lambda表达式中,表达式中可以访问变量、但不能修改变量。 使用=等号可以将函数作用域中的所有变量以值捕获的方式传入到表达式中。

(2)引用捕获:将外部变量以引用的方式传递到lambda表达式中,表达式中可以访问变量和修改变量;用&引用符号。

(3)混合捕获:捕获列表捕获多个变量,既有值捕获也有引用捕获。

(): 参数列表,可选,不需要传参的话()都不需要写;
->return_type: 返回值类型,可写可不写,编译器会自动推导;

{}: 函数体函数体内包含return之外的其它语句,则编译器推断返回类型为void。 这时候就需要自己设置返回类型。

最简单的lambda表达式:[] {};
lambda把一个函数 变成了 匿名可调用的代码块,
参数列表 和 返回类型 可以省略。
捕获列表 可以 捕获值,也可以捕获引用。
返回类型 可以自己推断(根据返回值)

lambda底层原理:
编译的时候,编译器把这个表达式翻译成一个未命名类的对象,该对象包含一个operator操作。 参数都包含在这个类成员里面。对lambda的调用,就是对这个类对象的调用。

lambda 优势:
使用时候去定义,不用跳出当前函数,定义时立即执行,(适用于小逻辑的函数)

C++模板
泛型编程的一种实现方式。
实现功能基本相同,仅仅是数据类型不同时,可以用模板。

函数模板:
定义一个通用函数,所用到的类型(参数类型、返回值类型、局部变量类型都可以作为参数),用一个虚拟类型代替。这个通用函数就叫做函数模板。

一般放在全局, template关键字告诉编译器开始泛型编程。

隐式实例化,自动推导;
显示实例化,告诉用哪个。

make-shared 和 new的比较:
1.先new,再赋值给shared_ptr; 2.直接用make_shared创建指针。

1.内存:
方法1:采用new,先new再赋值,会导致内存碎片化;(new在堆上分配一块内存,然后在堆上再建一个智能指针控制块,这两个不连续,所以内存碎片化)
方法2:maked-shared不会产生过多碎片内存。(直接在堆上建一个很大的内存,包括数据和控制块一起,然后用a的构造函数去初始化内存)

new 至少执行两次分配,一次用于对象 A 的创建,一次用于智能指针 shared_ptr 的控制块,而如果用 make_shared() 方法通常只需执行一次分配,效率上有所提升。

2.时间:make-shared只分配一次内存, new是分配两次内存,make-shared时间会快一倍。
c++ 11 加了move语义,(move太复杂了。。。)

虚函数:
memset初始化类对象会报错:
初始化obj的时候,将obj包含的指向虚函数表VTBL的指针也清除了。包含虚函数的类对象都有一个指向虚函数表的指针,此指针被用于解决运行时和动态类型强制转换时虚函数的调用问题。
解决方法:有虚拟函数的类对象,决不能使用memset来进行初始化操作。而是要用缺省的构造函数或其它的init例程来初始化成员变量。

虚函数有几个指针:
1.每个类都有虚指针和虚表;
2.如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针;
3.如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。

多继承:多个直接基类中产生派生类,多继承的派生类继承了所有父类的成员,多个基类相互交织会产生复杂的设计问题,比如命名冲突。(比如菱形继承)还会冗余数据。(不建议使用多继承,最好用虚继承)
虚继承:解决继承时的命名冲突和冗余数据的问题。。class B:virtual class A{}
让类声明承诺愿意共享它的基类,被共享的基类叫做虚基类。虚基类在继承中出现多少次。派生类中只包含一份虚基类的成员。

虚函数存在哪:
在C++中,虚函数表在编译期就已经确定,所以既不在堆上,也不在栈上,而是存在于编译后的可执行文件里面。堆和栈只有到了运行器才开始分配的。
不过在每一个有虚函数的类的对象的存储空间中都有一个虚函数表的指针,这个指针是在运行时确定的,用于实现多态。

C语言字节对齐——-
大多数64位的linux、unix系统都是LP64模型;32位Linux系统是ILP32模型;64位windows使用LLP64模型。
基本数据类型的地址必须是某个值K(2 4 8)的倍数。

字节对齐原因:
(1)CPU访问数据的效率问题,提高访问效率;(原因:处理器每次读出的字节数固定,字节对齐时和处理器情况保持一致,减少(从磁盘)读取的次数。。)
(2)节省存储空间,()
(3)有些系统不对齐的话,会读取失败。报错。。

如果做到字节对齐:
(1)基本数据类型,在特定平台上固定长度。
(2)结构体或类,对齐成员中值最大的那个值。
(3)

标准类型,地址是自身长度的整数倍就行。

什么时候需要字节对齐:CPU通信协议,硬件驱动程序,寄存器。。


构造函数 不能是虚函数,初始化时默认调用的,不可能通过父子指针去引用。
(构造函数在创建对象时自动调用,虚函数要通过父类指针或引用去调用,这个时候对象实例还没创建呢)
析构函数:一般都必须是虚函数,为了避免内存泄漏,析构时必须调用子类自己实现的析构函数用来释放。(不存在继承关系的话,单独一个类本身不需要虚函数)不用虚函数的话,继承关系中,可能只调用基类的析构函数,派生类对象不会释放内存。
类析构 顺序:(1)派生类本身的析构函数;(2)对象成员析构函数;(3)基类析构 函数。
析构函数默认不是虚函数:虚函数会浪费内存空间(需要的话自己设计生成虚函数即可)

初始化参数列表 和 构造函数体 的 区别:
构造函数,通过初始化列表和函数体内赋值,都可以实现对成员变量赋值。
构造函数初始化成员列表:是对类的成员初始化;const,引用类型,只能初始化不能赋值,必须使用初始化列表; 只是一次调用缺省构造函数,不会调用赋值操作,减少了不必要的开支,类很复杂时的作用就很明显了。
构造函数体内的逻辑:只是对类的数据成员的一次赋值操作。 使用时,调用了缺省构造函数,并且还调用了赋值操作,增加了开销。

C++ 类的初始化操作 有 4部分
1.初始化列表:所有类的非静态成员,都可以在这里初始化;类的静态成员不能在这里初始化。普通成员变量,const,没有默认构造函数的,

2.构造函数体:普通成员变量,需要复杂运算的初始化变量。

形参 和 实参(函数传参):
形参:是函数声明和定义中的参数值,是空值,没有实际数据;
实参:是调用函数时,把实际有数据的值传到函数里。
函数参数传递的3种方式:
值传递:实参传递给形参的是实际的值,形参和实参 在内存上是两个独立的变量(函数中是在临时分配的栈上操作,函数执行完形参就消失了),对形参做任何修改,不会影响实参。(不希望改变影响调用者时,使用值传递)
引用传递:实参传给形参的是堆内存上的引用地址,实参和形参在内存上指向了同一块区域,(形参添加一个&符号,表示是引用,只有C++支持,形参相当于实参的别名,对形参的操作都相当于实参的操作)
指针传递:形参为指向实参地址的指针,对形参的指向操作时,相当于对实参本身进行操作,外部实参也会被修改。
在这里插入图片描述

函数参数的压栈顺序:从右至左(栈低是最右边参数,栈顶是最左边参数),对于变长参数也好处理,栈顶看到的都是首参数。

如何理解指向指针的指针:
指针本质是保存地址的寄存器,保存一个地址;
指向指针的指针,就是指向地址。为了获取地址。

C++ 中的链接库
很多程序都需要依赖基础的底层库,库是可执行代码的二进制形式,被操作系统载入内存执行。
静态库(linux是 .a格式, windows是 .lib格式)
动态库(linux是.so格式, windows是.dll格式)

静态库:编译链接时载入,链接时,库完整的拷贝到可执行文件中,多次使用就会有多份冗余的拷贝;生成可执行程序后,静态库就不需要了。 ,每次都要全量更新。
特点:编译链接时执行,移植方便(直接拷贝, 运行时不再依赖); 浪费资源空间(所有相关目标文件和牵涉的函数库都会被链接合成一个可执行文件);静态库更新时,会很麻烦,所有依赖该库的程序都要再重新编译(全量更新。)

动态库(又叫做共享库):程序运行时 才被载入,供程序调用;系统加载一次,便可以被多个程序共同使用,共享库实例;增量更新。

数据类型:
计算机底层存储,本质是二进制,
位运算符总结:
&:与运算符, 2者都为1,则结果为1;
| :或运算符,有1位为1,则结果为1;
~: 非运算符, ~0 = 1, ~1 = 0
^: 异或,2者不相同,则结果为1.
<< : 左移符号,各二进位 全部左移 若干位, 高位丢弃, 低位补0;
–>>: 右移符号,各二进位 全部右移若干位,高位补0,
。。。。
常用位操作:
判断奇偶:
x&1 == 1, 等价于 x%21, (看最低位是奇数还是偶数)
x&1 == 0, 等价于 x%2
0,

x>>1, (右移1位), 等价于x/2,
获取x第n位的值,(x>>n)&1(右移n位,最低位正好是第n位,和1与操作,得到第n位是1或0)

位掩码(Bit Mask):类似子网掩码,屏蔽输入段,

位运算练习题目:
5亿个int数字,找到中位数,内存有限。
(1) 分治思想:从高位到低位,先比较最高位,0和1分治,0的数字大于1的数字,通过个数统计便可以判断中位数位于哪一堆。。
以此类推,继续比较次高位,对小规模数据进行分治。(根据每一堆的个数。。)

数据量小时,维护2个堆;大顶堆的最大值,小于 小顶堆的最小值,
偶数个数时,结果为两数之和; 奇数个数时,结果为多数堆的堆顶元素。

C++ 不支持二进制的表示,支持8进制,10进制,16进制。
(以0开头的数字,就是8进制。0x或者0X开头,代表16进制。其它整型数字,默认都是10进制。。)
int类型:默认是signed int,有符号类型,最高位是符号位,数据仅仅31位。
UINT32:是unsigned int的别名,无符号整数,最高位也是数据位,占32位。typedef unsigned int UINT32
INT32: 表示有符号的整数,32位有符号。

int32_t, 根据不同机器占据不同的位数。。 还有uint32_t…

atoi函数:将字符串转换成整型数,(ASCII to Integer), 只转换整型数字的字符串。

to_string(), 数字转字符串。

string.c_str(), string转const char*类型。

字符转数字: 直接进行计算,每个字符有对应的ASCII数值。。 比如:‘A’ + 1 = ‘B’

1.C/C++中的内存
操作系统中,会先把高地址的部分内存空间分配给内核(内核空间给内核线程执行特权操作),剩余的作为内存空间。
内存分为5个区:(C++堆栈内存空间,程序运行时系统分配使用。)
内存从高到低: 栈 -> 堆 -> 全局/静态/常量存储区 -> 代码区

(1)堆:(堆空间一般比栈大很多)
堆(heap)是C语言和操作系统里的术语概念,操作系统维护的一块动态分配内存,比如malloc和free,就是对堆内存的动态申请和释放。(生存周期由程序控制。。主动创建。。属于动态内存分配。。一般发生在程序调入和执行时)(会造成内存碎片)向内存地址变大的方向分配生长。
(–动态内存分配:按需分配,充分利用内存空间,及时释放,
在程序运行时完成,分配释放要占用cpu资源,要用到指针和引用,)
-----操作不当,会造成内存泄漏(memory leak, 程序未能释放掉不再使用的内存。失去对该段内存的控制,造成内存浪费。)
----内存泄漏的3种情况:1.堆内存泄漏(heap leak), 申请的内存没有及时释放掉;2.系统资源泄漏(resource leak), 程序使用系统分配的资源,没有使用相应函数释放掉(比如socket, handle, bitmap),系统资源浪费,系统性能降低不稳定;3. 基类析构函数没有定义为虚函数,子类资源没有被正确释放掉。

内存泄漏问题:最好使用智能指针(C++)
智能指针:
智能指针是一个类(类都是栈上的对象,所以函数或程序结束时会自动释放),类的构造函数 传入一个普通指针,析构函数释放传入的指针。
C++11引入的,memory头文件
unique_ptr,独占式指针,不支持复制和赋值,赋值使用move。(具有排他性,指针指向的对象只能一个指针去进行操作使用)
unique_ptr具有排他性,所以出现了shared_ptr.
shared_ptr,强智能指针,共享型的智能指针,维护一个引用计数(在堆上内存控制快),多个指针拷贝指向同一个堆内存,可以随意赋值,引用计数为0时才会释放堆内存。引用计数器是线程安全的,指向的对象访问会加锁。

引用计数器的线程安全性如何保证? 每个都是原子操作!!
shared_ptr的所有对象通过指针共享引用计数器(实质是一个指针),属于临界资源,
比较简单的一些操作,可以设置为原子操作来保证线程安全。
原子操作:不会被线程调度机制打断的操作;从开始运行到结束。原子操作是无锁的,通过CPU指令实现的,加上LOCK。
**原子操作底层:**也是对缓存加锁、总线加锁来实现多CPU之间的原子操作,CPU指令层面。 原子操作效率更高,底层硬件支持的。

最佳用法:定义对象时,用强智能指针shared_ptr; 引用对象时用弱智能指针(weak_ptr)

weak_ptr,弱引用指针,shared存在指针互相指向,成环的问题,会造成内存泄漏(a指向b,b指向a,引用计数无法为0,内存一直没法释放)。weak打破循环引用,只引用,不计数(不计数的话,就不会循环引用了)。 指向shared管理的智能指针,weak-ptr绑定到一个shared对象,资源观测权。不共享指针,不操作资源,不改变shared的引用计数。 expired(判断指向对象是否被销毁),lock(绑定shared,返回智能指针,可以判断指针是否为空),use_count(可以得到shared引用个数)
weak_ptr不能保证引用内存有效,使用前要检查为空。
把普通指针封装成栈对象,生命周期结束后,析构函数中释放掉申请的内存。管理堆上分配内存。最常用的是shared_ptr,引用计数法,是一个类,make_shared传入指针, 当引用计数为0时才自动释放引用内存资源。
智能指针删除器:不自定义的话,会有默认删除器。 引用计数为0时,调用删除器。(资源不是常规途径来的,要自定义删除器)unique自定义删除器时,要指明删除器类型;shared指定删除器对象即可,不需要指明类型。
unique_str类里面,删除器是其中一部分(默认的删除器是无状态的)不能推断删除器类型;shared总是具有删除器的状态容量。

(2)栈:(一般就多少M,很小,操作系统维护,)(栈在C++内存最高位,从高地址往低地址写入。)往内存地址变小的方向分配生长。
存放的内容: 1.程序运行时的局部变量, (2)函数调用时,栈用来传递参数和返回值,栈先进先出,放在保存和恢复调用现场。
用来保存函数内定义的非static对象,如局部变量(或者函数参数),仅在对象定义的程序块内运行才存在。。(被动的创建。。)

静态内存分配(由系统释放,相对于堆,大部分都是静态内存分配): 可能很多在栈空间中,或者常量存储区、全局静态存储区。。比如数组。。在整个程序中固定不变,。事先已知大小,由编译器负责,(大多发生在编译和链接过程中就申请好的,全程固定不变)

(3)程序代码区:存放二进制代码。。
(4)全局静态存储区: 存放全局变量、static静态变量(系统释放)
static: 静态全局变量,在全局数据存储区上分配内存;未经初始化的静态全局变量会被初始化为0。 变量声明在整个文件都是可见的,文件之外不可见。
全局变量。(可以被外部文件使用)
(5)常量存储区。 存放常量。(结束后系统释放)

(6)C++的自由存储区。
是C++中new和delete动态分配和释放对象的抽象概念,new申请到的内存区域叫做自由存储区,C++编译器默认使用堆来实现自由存储,new和delete底层使用malloc和free来实现,说在堆上也对,在自由存储区也对。

new和malloc的区别:
1.new是关键字,需要编译器支持;malloc是库函数,需要头文件支持。
2.new申请内存时,无需指定内存大小;编译器根据类型信息自动计算;new还会调用构造函数初始化内存;malloc需要指定内存大小;malloc只管申请分配内存,不会初始化。
3.new的效率高于malloc。malloc/free没有构造、析构函数。

new分配的内存用delete释放;new[] 分配的内存用delete[]释放。 (配套的!!)
不能混用,new和delete分别调用了构造函数和析构函数, malloc和free只是分配内存和释放内存操作。
混用的话,比如用new申请内存,free的话,就少了个析构函数操作。 操作不配对!!
malloc和free配套,free传递前面malloc的 指针对象就行了,不用传内存长度。

delete和delete[]区别:
delete[]会调用每一个数组元素的析构函数,释放每一个元素空间;delete只会调用数组头元素的析构释放空间。
1.对于基本数据类型,没有析构函数,用delete和delete[]没有什么区别。
2.对于类对象的数据类型,申请数组,一定要用delete[]来释放每个对象空间。

malloc的原理:????(本质是内存管理)
malloc函数的功能:是在内存的动态存储区中分配一个长度为size的连续空间。返回值是指向所分配连续存储区域的起始地址。
将可用的内存块连接成长长的链表,沿着链表找到用户申请的大小空间,一分为二,一份给用户,一份空暇;free释放时,把用户内存块又链接到空暇内存上。
操作系统层面原理:(1)开辟的空间小于128k时,调用brk函数,移动指针enddata(空间堆段的末尾地址)
(2)开辟的空间大于128k时,调用mmap函数,在虚拟地址空间(堆和栈空间,文件映射区域)找一块空间来开辟。(mmap可以单独释放。brk造成内存碎片。。)
这两种方法分配的是虚拟内存,操作系统分配物理内存,然后建立虚拟和物理的映射。(去使用这个内存时,才能进行物理内存的映射。。)

进程分配内存有两种方式,分别是由2个系统调用完成的:brk和mmap


段错误:访问的内存超出了系统所给这个程序的内存空间。存储器访问权限冲突。原因:使用了野指针,指向一个已删除的对象或未申请访问受限内存区域的指针)


内存溢出(Out of Memory, OOM):程序在申请内存时,没有足够的内存供申请者使用。
栈溢出(Stack Overflow): 递归或循环嵌套层次太多造成的,解决方法:该用堆。增大栈空间,该用动态分配。
堆溢出(heap …):只有一种可能,不停的new,但是没有销毁。。。

计算机负数、浮点数:
原码、反码、补码主要针对二进制而言。
对于正数没啥区别,引入反码、补码,主要是为了减法方便,(减号转换为负数,负数化为补码求加法)
负数存储:一个标志位1和补码表示(补码是反码+1)
浮点数存储:单精度类型(float)占32位和双精度类型(double)占64位,遵从ieee固定的标准存储。
单精度32: 符号位1+指数位8+尾数部分23
双精度64:符号位1+指数位11+尾数部分52

C++ 各数据类型取值范围:
char, 1个字节,占8位,
unsigned char, 无符号字符,都是正值,0 ~ 2^8-1 0 ~ 255 (全用来表示数值)
signed char, 有符号字符,分正负,-2^7~ 2^7-1 -128 ~ 127 (一个符号位)

整型,
int, 4个字节, 占32位,
unsigned int, 无符号整型,都是正值,0~2^32-1
signed int, 有符号整型,分正负,-2^31 ~ 2^31-1
short,2个字节,占16位。
unsigned short, 0~2^16-1
signed short, -215~215-1
long, 4个字节,占32位。和int一样。
float,4个字节;double,8个字节。

  1. 程序编译过程
    -译处理(.c/.cpp变为.i),源程序文本变成被修改的源程序文本。(处理#include,拷贝到源程序中,#define宏替换,没用的注释删除掉。。)
    #开头的都是预处理指令,#include,#define, #if #else(条件编译,,)
    预处理时,头文件 和 宏定义,直接内容替换。。。
    条件编译(程序规模很大时,很有用)的话,就按实际条件,来进行编译。。

-编译(.i变成.s),源程序文本变成汇编程序文本。(词法分析,语法分析,语义分析,生成汇编)
-汇编器(.s变成.o),汇编程序变成可重定位的目标程序(机器语言,二进制)
链接器(.o变成二进制),变成可执行的目标程序二进制文件。(把需要用到的库链接到一起,比如printf在libc.so.6里面。)

3.函数调用的原理
大体过程:参数入栈、函数跳转、保护现场、恢复现场。
(1)函数调用中的3个寄存器,
(ebp,esp, eip,3个系统寄存器,和栈相关;栈顶-低地址区,栈底-高地址区,栈填充从高地址往低地址增长。)
eip:存储下一条指令地址(每执行一条指令,该寄存器变化一次)
ebp:存储当前函数栈底的地址,栈底是基址,可以通过栈底和偏移计算来获取变量地址。
esp:始终指向栈顶。
(2)栈中存储的顺序和内容:
参数拷贝(压栈),参数顺序从右向左(push指令,参数入栈) -> 保存函数调用的下一条指令,(指令跳转,函数跳转,跳转函数体内,EBP也是该函数基地址)

(3)保存现场,(保存返回地址)
EIP压栈(下一条执行指令地址), 函数返回时,从栈中弹出该值,继续执行。
会把EBX, ESI, EDI压入栈中,保护现场的一部分(分别存储基地址,源地址寄存器,目标地址寄存器)

(4) 执行函数, 局部变量。
局部变量也是存到栈上,基于EBP基地址计算来存放。

(5) 恢复现场
函数执行完毕,做后处理操作,从栈顶读取EBX, ESI, EDI,恢复现场; 内存释放, 恢复断点,栈的指针和寄存器都恢复到函数调用前的状态,

移动ebp,esp,形成新的栈帧结构,
压栈,
return值
出栈,
恢复main的栈帧结构,
返回main函数。

4.C++ 左值和右值
左值:可以放在赋值符号左边的变量,具有相应用户可访问的存储单元,也可以改变其值。存储在计算机中的对象,也代表一个地址,通过地址可以对内存中这个左值对象进行读写操作。

右值:右值相当于数据值,可以是个常量没有地址(符号常量放在操作符右边)(临时的,通常生命周期在某个表达式内)
(所有左值也可以认为是右值,右值不能是左值)

语言语法相关

  1. C++和C,
    const:在c和c++中差距很大。
    c语言的const:修饰变量之后还是变量,只读。

c++的const
const修饰过后,变量就变成常量了。当常量用,固定不可变。(const常量会被加入到符号表,会查表,所以不可变。)值在编译时就已经定了。
尽量使用const,可以帮助我们避免很多错误,提高程序正确性。

const和宏定义区别:宏是在预处理阶段替换的,const是在编译阶段固定的。
(1)const变量: const变量必须初始化, 变量值是只读的,不能修改。
(2)const类对象:此类对象不应该被改变。 const修饰类对象,类对象任何成员值都不能被改变。
(3)指向const变量的指针: 指针指向内容不能改变,但是指针指向可以改变。
(4)const指针:一个指针经过const修饰,指针指向不能改变,指针指向的内容可以改变。
(5)const变量作为函数参数: 限制函数体内,不能改变这个参数值。传参时,不要求必须是const,但是在函数体内会转换为const。
(6)const返回值: 函数返回const引用。 返回值不能在外面被修改。
(7)类中成员变量为const:const成员变量必须在构造函数中被初始化。类中成员变量为只读,不能被修改(包括在类内部和外部)
(8)类中const成员函数: 此函数不应该修改任何成员变量。传给const成员函数的this指针,是指向const对象的const指针。

符号表:语义分析。。hash表。。
————
宏和内联(inline)
宏是C语言的一种预处理功能
内联(inline):是C++引入的一个关键字,C++中推荐使用内联函数inline来代替宏。
内联发生在编译阶段(会有参数检查、返回值检查),会更安全。宏只是在预处理,
内联函数,一般定义在头文件中。(只在当前文件中生效,可以被其它文件include去调用)
调用普通函数(比求解等价表达式)效率会低很多,
内联函数:为了提高函数执行效率,
内联函数原理:编译时,编译器将内联函数在调用点上源代码展开,inline放在函数定义前,内联函数将它在程序中的每个调用点上“内联的”展开,(
普通函数调用开销很大,参数压栈、跳转、返回值,寄存器处理等过程),频繁调用函数效率很低。
内联函数缺点: 调用点过多的话,代码冗余。
对于小而精、并且需要频繁调用的函数,尽量使用inline内联处理,可以提高效率。
注意点:函数少于10行时使用内联;递归函数、虚函数不能内联。。

—————
有了malloc和free,为啥还要new/delete??
malloc和free:是C语言的标准库函数,需要相应库支持,某些平台可能不支持;
new/delete是C++语言自带的运算符,不是库函数,对于对象来说需要,malloc和free不能满足需求,new可以初始化。

——————
C++提供了4种类型强制转换操作
const_cast:常量转换,强制去掉不能被修改的常量特性,一般用于常量指针或者引用。
static_cast(类似C的强制隐式转换,静态类型转换,
(1)继承之间的父类和子类之间的互相转换,都不会报错;派生类转基类是安全的,基类转派生类,由于没有动态类型检查,是不安全的。
(2)基本数据类型之间的转换,需要开发人员自己保证安全性。int、char等等。

dynamic_cast(动态类型转换,有条件转换,会检查类型安全,类指针间的转换,需要虚函数支持)(最安全的转换!!!)
主要用于继承中基类和派生类之间的互相转换,基类转派生类时不安全会报错。

reinterpret_cast(重新解释的意思):指针或引用 和 整型之间的互相转换,转换时,字节逐个拷贝,提供了最大的灵活性,但是安全性很差。
————————-
static:(作用于静态变量,静态函数)
1.面向过程中使用,加上static,定义全局静态变量、局部静态变量。
**静态全局变量:**在全局数据存储区,分配内存;未初始化的会默认初始化为0;静态全局变量在整个文件里都是可见的,文件之外不可见。
函数退出时,全局数据区的数据并不会释放空间。
静态全局变量和普通的全局变量的区别:都能实现文件内共享,静态全局变量不能被其他文件所使用。
普通全局变量,会被其他修改使用,不受控制。

静态局部变量:
普通局部变量:每次运行到变量时,都会在栈上分配内存;函数体退出时回收栈内存,局部变量就失效。

静态局部变量:保存在**全局数据存储区,**局部变量值也可以一直保存,下次函数调用时还能读到,继续赋新值。
第一次调用时被初始化,后面就不再初始化。始终在全局数据存储区,直到程序结束。作用域为局部作用域,

静态函数:
在函数的返回类型加上static关键字,函数就被定义为静态函数。只能在声明的这个文件中可见,其它文件不可见。优点:1.其它文件不可用;2.其它文件也可以定义同名函数,不冲突。

2.面向对象中的static:类中的static
(1) 类中的静态数据成员:比如,static int sum; 静态数据成员属于类的成员,只有一份拷贝 只分配一次内存,类的所有对象共享该静态数据成员。 (也保存在全局数据存储区)
在没有产生类的实例时,就可以操作它。
静态数据成员定义时要分配空间,所以不能在类声明中定义。
使用场景:(1)类的所有对象都有相同某个属性,可以使用;在全局区共享,节省存储空间;(2)需要改变时,改变一次就可以了。

(2)类中的静态成员函数:与类的静态数据成员一样,都属于类的一部分。
静态成员函数没有this指针,不与任何对象相联系,通过类名可以直接调用。
静态成员之间,可以互相访问,静态变量和静态成员函数。
非静态成员函数,可以访问静态的;
静态成员函数,不能访问非静态的。
优点:方便,不需要创建对象就可以使用。 没有this指针开销,静态成员函数速度会快点。

static作用:
1.隐藏作用,文件内共享,对其他文件隐藏;普通的全局变量,通过extern,就可以被外部文件随意调用。
2.名字冲突问题:静态数据成员,不会进入程序全局名字空间,不存在与程序其它全局名字冲突的可能性。

1.隐藏的作用, 未加static的全局变量和函数是全局可见的(可以通过extern), 加入static,就会对其它源文件隐藏,只在当前文件用 其它文件用不了。
2.保持变量内容持久:静态存储区的内容,在程序运行时就完成初始化,唯一一次初始化,全局变量和static变量在静态存储区,生存周期为整个源程序,但是作用域和普通变量相同。存在但是不能用。
static作用于类,类的静态数据成员,类的静态成员函数。
类的静态成员变量:类内声明static,类外单独分配存储空间,不依赖类的某个对象,所有对象共享静态成员变量,通过类名直接调用静态成员变量;
静态成员函数:类共享,可以访问静态成员变量,不能直接访问普通成员变量,类名直接调用静态成员函数

——————
extern:变量或函数声明前,说明在别处定义的,在此处引用。
作用:1.引用同一工程下的其它文件里的变量;2.引用后面作用域的变量,通过extern扩展作用域。
弊端:被引用后,可以修改该变量,会影响其他地方使用,所以不太安全,慎用(所以就有了static)
引用其它文件的变量和函数有2种方法:1.#include(引用整个文件里的接口调用),2.extern(只引用某个变量)

extern C
作用:调用C库时使用,因为C语言和C++的编译差异。
extern C 放到函数或变量前面,告诉编译器:(1)这个函数或变量是extern类型的;可以在本模块或其它模块使用;
(2) extern C: 表示该函数编译生成的符号表是C语言风格,_foo(不包含参数类型和返回值类型)。
C++编译器:编译链接的函数符号表,_foo_int_float。
C++编译的时候把函数和参数联合生成中间函数名称,C语言不会(会造成链接找不到对应函数),
C++调C库,告诉编译器不要修改C库函数名。 会导致链接差异,找不到符号。

————-
头文件,
C++条件编译, (作用:1.防止头文件过多重复包含,2.调试使用,3.不同角色或场景判断使用。)
#ifndef (if not define…的缩写,如果没有定义),一种宏定义判断,防止头文件被过多重复包含。
#define
#else
#endif,

和program once
都是为了防止头文件被重复包含。作用于包含起来的某一段代码。
#ifndef和#program once的作用都是一样的,都为了防止头文件被重复包含。。细微区别如下:
(1)ifndef是编程语言支持的,可以针对部分代码片段,编译速度特别慢,效率低,要不停的检查头文件引用情况。(更灵活,兼容性好,移植性好)
(2)program once是编译器支持的,目前不同版本编译器支持的不够好。 作用于包含该语句的整个文件(无法对某一段代码做program once声明),操作简单,效率高。

—————-
指针,引用

  1. 指针:是一个变量,这个变量存储的是一个地址,可以通过访问这个地址进行内容的查询和修改;
    引用:只是一个别名,还是被引用的变量本身,对引用的任何操作,都是对变量本身的操作,可以达到修改变量的目的。 (本质上是一个指针常量!!const指针 不会单独申请空间,而是和被引用变量指向同一个地址。)
    共同点:都是地址概念,都是指向一块内存,
  2. 指针:可以不初始化,引用必须初始化。定义引用时,必须把该引用和初始值绑定在一起。(引用不能为空)
    引用的内部实现是借助指针来实现的,一些场合下引用可以替代指针,比如函数形参。

3.引用只有一级,指针可以多级指针。
4.传参:指针传参时,指针本身的值不可以修改,通过指针指向才能对对象进行操作。
引用传参时,传的就是变量本身,通过引用,可以随意修改这个变量。

引用:就是基于指针,加一些限制比如必须初始化、不能改变引用指向。
引用是别名,在汇编层面 和指针并没有区别,引用会被C++编译器当作const指针。
引用占用的内存空间 和 指针一样,因此,引用内部是通过指针完成。


struct和union:
struct:结构体,将不同类型数据组合起来,成为一个整体,属于一种自定义数据类型。
struct sizeof计算,数据对齐问题。struct的内存布局:依赖CPU,操作系统,编译器编译时的选项。
struct中,各成员都占用自己的内存空间,都同时存在,struct变量总内存长度 = 各成员内存长度(对齐后的)之和。
对齐规则:
1.第一个成员在结构体变量偏移量为0的位置,(起始位置为0)
2.其他成员要对齐到某个数字(对齐数)整数倍的住址处。
对齐数:min(编译器默认对齐数 ,成员大小),比如VS默认对齐数为8。 每个成员计算出自己的对齐数。
3.结构体总内存大小为最大对齐数(每个成员都有一个对齐数)的整数倍。 (找到最大对齐数,作为整个结构体的对齐数) 要做最后的检查!! 不是整数倍的话,要补齐。
(改变成员前后顺序,因为每个成员对齐数不同,摆放住处不同,可能导致整个结构体大小不一样。)

大端(Big-Endian)和小端(Little-Endian)
只是数据在地址中的不同存储顺序。
char长度为1,没有高低字节之分。大于1的字节的类型,才有高低之分。
小端模式:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端;
大端模式:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
大端:左边都是有数据的,右边可能为空;小端:右边都是有数据的,左边可能为空。
读取时,大端依次读出,小端倒叙读出。
大端模式:更符合人理解的模式,符号位在内存的第一个字节,可以快速判断数据正负和大小;
小端模式:更符合机器性能模式,低地址存放低字节,高地址存放高字节,强制转换时不需要调整字节内容。
union(联合体):采用小端模式,从低地址开始存放。
网络中,TCP/IP,规定必须采用大端模式,网络字节顺序(Network Byte Order,NBO)。
CPU中,都是采用小端模式。
芯片中都是采用大端模式,符合阅读习惯(这个不确定??)

为什么要内存对齐?
1.硬件平台要求,比如int,有些只找4倍数位置的数字。不对齐的话,可能找不到。
2.经过对齐后,CPU访问数据的速度大大提升。按照对齐数一个一个去读的,不对齐的话,要访问2次才能拿到一个数据。 空间换时间,提高访问速度。
缺点:空间资源浪费。

union: 共用体,又叫联合体,将不同类型的数据变量存储在同一段内存单元。union所有数据成员从同一个地址开始存储,union的内存大小 = union中占用内存单元最多的数据成员大小。
优点:使用频率没有struct高,节省存储空间。每次赋值一个成员变量,会被重写覆盖。

优化struct结构体的空间资源浪费:
__packed:进行一字节对齐。完全紧凑起来。
#pragma pack(4): 修改系统对齐字节数,
会降低系统运行性能,CPU访问数据的速度变慢。

class内存空间
1.函数放在代码区,数据主要放在堆和栈上,全局静态区或者常量区;
2.默认空类为1,
3.也要内存对齐,和结构体一样。

——————-
#面向对象
封装:
数据和函数集合在一个个单元中,称为类。保护或防止代码数据被破坏。
哪种属性,类内都可以访问。
public:暴露手段,暴露接口 类的对象都可以访问
private:隐藏手段,类的对象不能访问。
protected:和public一样可以被子类继承,和private一样不能在类外被直接调用。

继承:
主要实现重用代码,节省开发时间。子类继承父类的一些东西。
公有继承:派生类成员状态和基类完全一致
私有继承:派生类成员状态都是私有(私有不能继承)
保护继承:派生类成员状态都是保护(私有不能继承)

多态
多态是设计模式基础,是框架基础。多种状态。接口的多种不同实现方式即为多态。
多态三要素:同名函数、依据上下文、实现却不同。 运行时动态调用实际绑定对象函数的行为。

对象的静态类型:编译时确定的,程序中声明采用的类型(字面类型),静态绑定,前期绑定,(非虚函数是静态绑定,缺省函数是静态绑定)
对象的动态类型:运行时确定的,动态绑定,实际类型,后期绑定,(virtual函数是动态绑定的,编译时生成虚函数表,运行时才知道调用哪一个函数。。)
两个概念一般发生在基类和派生类之间。
引用可以实现动态绑定。 引用使用时必须初始化,引用既可以指向基类对象,也可以指向派生类对象,这是实现动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。
———-
构造函数和析构函数
(顺序:最先创建构造函数,最后析构。跟栈一样。)
默认构造函数:没有任何构造函数时,编译器自动生成默认构造函数,即无参构造函数。
当类没有拷贝构造函数时,会生成默认拷贝构造函数,(默认是浅拷贝)
???
构造函数:处理对对象的初始化。是一个特殊的成员函数,不需要用户调用,创建对象时自动调用。
拷贝构造函数:对未初始化的对象存储区进行初始化;每创建一个对象时,都会开辟一个新的地址空间;相同类型的类对象通过拷贝构造函数完成复制过程。
默认拷贝构造函数(浅拷贝):只是简单的复制成员变量值。静态成员处理不到位(static引用计数次数不对)。还有动态申请内存的成员,两个指针指向同一个内存地址空间(析构时销毁2次)(浅拷贝。)
没有静态成员和动态申请内存的成员,可以用浅拷贝。
深拷贝:对象成员都重新申请内存空间,指向不同的地址,只是内容相同。动态成员会重新分配内存空间。(指向的内容是相同的。尽量少用默认拷贝。。)

析构函数:特殊的成员函数,编译器自动调用,对象不再使用时,清理资源的时候调用。

构造函数和析构函数,可以虚函数吗? 顺序? 问题!!
对于同一个类的不同对象而言,先构造后析构,后构造的先析构。


封装:可以隐藏实现细节,保护代码数据,使得代码模块化;
继承:可以拓展已存在的代码模块(类);
封装和继承的目的,都是为了代码重用。

多态的真正作用:派生类的功能可以被基类的方法或引用变量所调用,向后兼容,提高可扩展性和可维护性。 (多态是设计模式的基础。。)
多态(poly - morphism)
1.什么是多态?
从字面理解,不同对象,对同一行为(具有相同的函数),有不同的状态(函数内部实现不同)。

和重写(覆盖)相似的概念:overload、override、overwrite
1.重载(函数):overload,函数名称相同、参数不同,不考虑返回值,**在同一个作用域内,**减少函数名数量。 意义:提高代码可读性、代码复用、减少函数名冲突。 (不同作用域内不叫重载)
2.覆盖重写(override):派生类函数覆盖重写基类函数,基类必须是virtual类型。(这就是多态) 用来修饰派生类函数,表明我这个是要重写基类函数。
3.重定义(又叫隐藏)overwrite: 前提普通函数,不是虚函数。。继承关系中,子类实现了和父类名称完全一样的函数,子类就把父类函数隐藏了。

2.多态如何实现?
(1)虚函数重写(也叫做覆盖),重写就是设置不同状态。(基类和派生类之间,函数名、参数和返回值都完全相同,都是虚函数)
(C++提供override 和 final来帮助用户检测是否重写,防止疏忽写错,override在编译时会在子类函数检查是否重写正确(使用override是好习惯)。
final: 不希望某个类被继承或不希望某个虚函数被重写,使用final, 后面被继承或被重写时会报错。。)
(2)对象调用虚函数时,必须是指针或者引用。
代码上体现多态:当父类指针指向子类对象时,通过父类指针能调用到子类的成员函数。


虚函数:C++的多态主要通过虚函数实现,
虚函数是带有关键字virtual的成员函数。子类有个和父类完全相同的虚函数(函数名、形参和返回值都相同),子类虚函数重写父类虚函数,
-----------虚函数:????原理??
虚函数是C++实现多态的机制,多态:就是用父类型的指针指向其子类的实例,就可以通过父类型的指针调用子类的成员函数。
虚函数是C++实现 动态(dynamic)、单分派(single-dispatch)、子类型多态(subtype poly morphism)的方式。
动态: 运行时决定的。(静态:编译时决定)
单分派:(基于一个类型,去选择调用哪个函数。)(多分派,是基于多个类型去选择调用)


虚函数表:
虚函数的实现由:虚函数指针和虚函数表,两部分组成。
有虚函数的类,都会生成一个虚函数表,在编译时期生成(一般存放在常量区、代码段中。。)。虚函数表是一个存储虚函数地址的数组。以NULL结尾。
虚函数表指针,存放在对象中;虚函数和普通函数一样,存放在代码段中。
1.对象空间中的,虚函数指针(virtual function pointer), 拥有虚函数的对象,会多出一段空间,存放虚函数表的地址(指向虚函数表)。。指向函数的指针,指向用户所定义的虚函数,(可以用基类的指针,去调用子类函数)
2. 虚函数表:虚函数表是最重要的,每个拥有虚函数类的对象实例化,都会拥有一个虚函数表,存放全部虚函数的地址,每个地址都相当于一个虚函数指针,并且排在对象的地址首部(保证正确取到虚函数的偏移量)。它们都是按照一定顺序组织起来的,构成一种表状结构,称为虚函数表(virtual function table.) (按照父类、子类的顺序排列保存)
通过对象实例的地址得到虚函数表,然后就可以遍历其中的函数指针,进行相应的函数调用。

一般继承(子类没有覆盖父类虚函数,这是毫无意义的,一般都是用来覆盖的。。 虚函数表中,父类函数在前,子类函数在后。)
覆盖情况下的子类继承(覆盖的函数放在表中最前面,调用覆盖函数时,实际调用的子类函数。。)
多重继承: 地址首部有多个虚函数地址(按照声明顺序), 覆盖的情况下,覆盖函数在每个父类地址中都有。。
级联继承:

虚函数的多重继承,级联继承,会产生哪些问题,怎么解决?? 问题!!!
多重继承: 一个派生类有两个或多个基类,从两个或多个基类中继承所需的属性,
优点:对单继承的扩展,更多样化的继承,
缺点:结构太复杂了,优先顺序模糊,功能冲突。
二义性: 多重继承会产生的问题(两个基类中数据成员名称相同)
二义性的解决方法:
(1)用类名做前缀,来调用成员。(2)好好设计类。

菱形继承: 多重继承的父类拥有共同的父类。
解决方法:用虚拟基类。 虚拟继承。
虚拟基类的作用是: 如果在一个派生类的继承结构中,存在两个同样的虚拟基类,那么该类的实例化对象内存中只存在一个虚拟基类的子对象。
保证了在多重继承时,子类实例中只存在一个虚拟基类的子对象。
public B : public virtual A ,

多级继承(级联继承): B继承A, C继承B, 这种不会有啥问题。很正常的继承方式,单向继承。

2.虚函数表(virtual function table.)

构造函数不能是虚函数!!! 虚函数表指针是在构造函数初始化列表阶段才初始化的;
析构函数可以是虚函数!!!不影响,所以可以。。(通过虚函数表地址,可以指引正确delete析构对象。)
虚函数不可以定义成static!! static不用对象就可以调用,但是,没有对象就可以虚表指针和虚函数表,(矛盾。。)
内联函数 不能是虚函数!!不能,内联函数没有地址(只是在调用点对代码展开。。)
对象访问普通函数,和虚函数哪个快?? 普通对象一样快,指针对象或者引用对象的话,普通函数更快,虚函数要去列表中查询地址,查表。。

虚函数和纯虚函数的区别:
纯虚函数:没有函数体,定义的时候函数名后面加上=0.
包含纯虚函数的类,叫做抽象类。抽象类无法实例化对象;抽象类的子类,必须把纯虚函数全部实现,才能实例化对象。
抽象类的作用:可以强制派生类必须重写纯虚函数。体现了接口继承。
(1)含有纯虚函数的类,叫做抽象类;含有虚函数的,不能被叫做抽象类;
(2)虚函数可以直接使用,也可以被子类重载后调用子类来用;纯虚函数必须在子类中实现才能使用,纯虚函数在基类中只有声明、没有定义。
(3)纯虚函数,只是一个声明抽象,一个接口。
(4)虚函数 virtual {}; 纯虚函数 virtual{}=0; 它们不能加static,虚函数是动态绑定, static是静态绑定。
(5) 编译多态性: 重载函数实现(函数名相同,参数返回值不同); 运行多态性: 虚函数实现(都相同。。)。。

move:移动语义
将对象的状态/所有权,从一个对象转移到另一个对象(只是转移,没有内存的搬迁或者拷贝)
RValue b = std::move(a); 移动构造后,b指向a的资源,a不再拥有资源,a被清空。
避免不必要的内存拷贝操作,提高效率,左值引用转换为右值引用,减少临时内存拷贝。
move原理:本质上是将传入的参数强制转换为右值引用类型,然后返回该引用。move本身不会移动数据,只是告诉编译器该对象可以进行移动操作。(改变了引用,本来A引用那个地址,换成了B引用)
(右值引用可以绑定到右值,右值引用可以修改)
move应用场景:(1)内存很大的变量A,换成变量B。(2)要将一个对象传参,然后该对象传完就不需要了。

volatile:(翻译过来,就是不稳定的意思)
和const正好相反。
告诉编译器,这个变量具有易变性,不可信,每次使用变量时都要重新从内存中读取一次。
不可优化性,不需要编译器对该变量优化;
顺序性,保证变量之间的顺序性。
volatile经常用于多线程场景。

程序调试和问题分析相关

google-perftools
针对C/C++程序的性能分析工具,可以对CPU时间片、内存等系统资源的分配和使用进行分析。
使用方法:(1)引用so的动态链接库,编译进去。
(2)在需要调试的代码前后,引用库函数的剖析模块的启动和终止函数,就能实现对这段代码的性能分析。
(3)落下了一个分析文件。(运行时间、内存大小)

gdb调试core
程序崩溃。对堆栈信息分析,了解程序运行状态,分析判断程序崩溃的原因。
(1)堆栈完整且稳定(指稳定出错)
(1.1)可以快速看出错误原因,堆栈行本身代码有错误,
(1.2)上下文代码有错误,缩小范围后前后多看几行就知道了;
(1.3)常规函数调用方式不对,传递野指针啥的。

(2)堆栈完整且不稳定(不稳定是指:程序时好时坏)
排查较为困难。往往和多线程有关,
先将并发数改为1,看看能否解决问题。
观察core的堆栈行的代码,是否存在多线程冲突。
加锁、加日志打印。
单线程后,还存在随机core,问题就转换为野指针类似的定位,

(3)堆栈被破坏。

gdb原理:
基于ptrace这个系统调用。

被调试程序和gdb之间建立跟踪关系,截获所有信号(hook),根据截获信号,查看被调试程序的内存地址,并控制被调试程序继续运行。

gdb常见的使用方法:
(1)断点设置

(2)单步调试

建立调试关系,2种模式:
(1)fork:利用fork+execve执行调试程序, 子进程在执行execve之前调用ptrace,建立与父进程之间的跟踪关系;
(2)attach:调试器调用ptrace,建立自己与进程之间的跟踪关系。

断点原理:
(1)指定位置插入断点指令,调试程序运行到断点位置时,产生SIGTRAP信号。信号被gdb捕获判断是否断点命中,等待用户输入进行下一步处理。(类似软中断)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值