C++笔记

1.0 类   

  1. 结构体对齐原则:1.第一个偏移量0  2.Min(默认对齐数,自身类型大小) 3. Max(对齐数)的整数倍 4.嵌套结构体(对齐到Max(对齐数)的整数倍)
  2. 内存对齐的原因:1.有的平台只能访问指定类型 2. 2次访问-->1次访问     求偏移量:OFFSET(struct,e)//类名,成名 实现以宏的方式#define OFFSET(struct,e)(size_t)&(((struct*)0)->e 
  3. 如何在类外访问类内的私有成员:友元函数
  4. class/struct区别访问权限继承权限,前者私有后者公有。class还用于定义模板类型参数。
  5. 类实例化:图纸-->房子  空类大小为什么是1 :为了使类对象具有独一无二的地址,有虚函数的类大小会+4 因为虚表指针,sizeof(类)static型成员并不计算在内:因为它在静态存储区/全局而sizeof求的是栈上的成员大小,静态成员为所有类对象共享如果占内存与普通变量无异。
  6. this指针: 类类型* const,指针而不时引用历史原因先有指针,this可以为NULL:class A{};  A* a=NULL只是不能通过该指针访问其成员。 const:左定值(指向的空间内容不变)右定向(指向的空间地址不变)
  7. 构造函数:explicit修饰为了避免构造出的对象出问题(强制类型转换),必须在初始化列表里初始化的成员变量(1.0 const/引用 2.0 类类型(有带参的构造函数)3.0 初始化父类的私有成员变量) 原因: 声明后立即需要初始 化而不是构造函数的赋值。
  8. 拷贝构造函数:参数类类型引用,减少一次拷贝?不是的实际上是为了避免无限递归的情况。
  9. 析构函数:对象销毁时资源的清空。不删除对象只清空资源。
  10.  静态成员:属于类,访问方式:类名::静态成员或者 对象.静态成员,static成员变量在类外定义声明时不加static,静态成员函数没有this指针不能使用非静态成员(变量或者函数)。
  11. const修饰类成员:修饰成员函数时实际修饰this: const 类类型* const this     对象的地址和其成员都不能改变,如果非得改变需在其成员前加上mutable关键字。
  12. 内联函数 :减少了函数压栈开销,inline建议性关键字优化后可能忽略,inline在函数定义之前,类内的成员函数默认为内联
  13. 宏:优点,高效直接替换,缺点,不进行参数类型检测有副作用,C++建议用const替代宏常亮,以inline替代宏函数。
  14. 友元函数:为了实现类外直接访问类的私有成员函数(类内声明类外定义)非成员函数,单向不传递,效率高破坏了封装性。 
  15. 操作符重载:不能重载的有  成员选择符.   成员对象选择符.*  条件操作符?:   域解析符: 成员对象选择符:class A{public:int c};  int A:: *ptr     A a; a.*ptr=10 <==>  a.c=10       >>和<<一定要重载为类的友元函数(满足习惯cout>>data而不是data>>cout);

2.0 动态内存管理

    C语言的动态内存管理

  1. 使用malloc,realloc,calloc进行动态内存管理(开辟),free释放空间。
  2. 内存空间:数据段,代码段,堆,栈,静态区。静态区存储未显式初始化的程序的全局/静态变量,数据段存储已经初始化的全局/静态变量,代码段存储执行程序在程序运行前已经确定,栈区系统自动分配程序结束自动回收,堆区程序运行时分配即动态分配 从低地址向高地址。                                                                                                                                                                                      低地址高地址
  3. malloc realloc calloc:void* malloc(size_t size)字节数 void * realloc(void * ptr,size_t size)  void* calloc(size_t size) calloc会把内存初始化为0   realloc内存不够时重新开辟一段空间将原来内存中的数据拷贝过去,malloc成功返回空间地址否则返回NULL。
  4. 堆和栈的区别:1.0 管理:内存碎片、效率,申请后响应:栈内存够返回给用户不够则报错overeflow,堆先在自由链表中找到对应的桶,头删返回该空间地址如果申请的空间内存大于索要字节数多余的挂载至自由链表适当位置2.0 生长方向 3.0 空间大小:栈空间大小固定,堆取决于系统可用虚拟内存的大小。4.0 分配方式:栈也有动态分配内存用alloca函数(原型同malloc) 
  5. 全局变量/全局静态变量/局部变量/局部静态变量:全局作用域单是全局静态变量作用域为当前定义它文件,局部静态变量只初始化1次作用域为定义它的函数体内。
  6. cha* p 和char[ ]: 读写权限,效率。
  7. 内存泄漏:释放对象错误,部分释放,二次释放,释放后再次访问
  8. 段错误:访问的内存超出了系统所给这个程序的内存空间                                                                                                                 通常导致段错误的几个直接原因:
             1、解除引用一个空指针(常常由于从系统程序中返回空指针,并未经检查就使用)。
             2、用完了堆栈或堆空间(虚拟内存虽然巨大,但绝非无限)。                                                                                               3、访问受系统保护的内存。int* ptr=(int*)0;    *ptr=100;                                                                                                     4、访问只读内存  char* ptr="usage"; strcpy(ptr,"stfu");                                                                                                             5、内存越界(数组越界访问(顺序结构))

    C++动态内存管理

  1. C++通过new和delete进行动态内存管理。new和delete,new[] 和delete[ ]定要匹配使用.
  2. 内存映射区:高效I/O映射方式,装载一个动态库,用系统接口创建共享内存实现进程间通信。mmap本身其实是一个很简单的操作,在进程的页表中添加一个页表项,该页表项是物理内存的地址。调用mmap的时候,内核会在改进程的虚拟空间的映射区域查找一块满足需求的空间用于映射该文件,然后生成该虚拟地址的页表项,改页表项此时的有效位(标志是否已经在物理内存中)为0,页表项的内容是文件的磁盘地址,此时mmap的任务已经完成。 当mmap建立完页表的映射后,就可以操作改块内存了,进行的所有改动都会自动写会磁盘文件。第一次访问该块内存的时候,因为页表项的有效位还是0,就会发生缺页中断,然后CPU会使用该页表项的内容也就是磁盘的文件地址,讲该地址指向的内容加载到物理内存,并需改页表项的内容为该物理地址,有效位置为1.
  3. C++是兼容C的,那么已经有C库malloc/free等来动态管理内存,为什么C++还要定义new/delete运算 符来动态管理内存:malloc/free职能开辟内置类型的空间,而自定制的类型对象的构建/删除需调其对应的构造和析构函数,所以有了运算符new和delete。
  4. malloc/free和new/delete的区别和联系:开辟空间的接口1.0 标准库函数,运算符2.0 只是开辟/释放空间new/delete还初始化和清理成员资源 3.0 malloc需要计算类型大小new自动计算,返回值 前者void* 后者 类型的指针。                                                          
  5. 定位new表达式    A* p=(A*)malloc(sizeof(A))   new(p)A(init_value)   p:类型的指针   init_vale初始化列表
  6. opreator new的内部实现机制:                                                                                                                                                                                                                 不调用构造函数的情况:类型为内置类型/未显式定义构造函数且编译器未合成默认构造函数。                                             delete实现机制:                                                                                                                                                                            
    void operator delete(void *pUserData )
    {
            _CrtMemBlockHeader * pHead;
    
            RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    
            if (pUserData == NULL) //内存不存在直接返回
                return;
    
            _mlock(_HEAP_LOCK);  /* block other threads */
            __TRY
    
                /* get a pointer to memory block header */
                pHead = pHdr(pUserData);
    
                 /* verify block type */
                _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
    
                _free_dbg( pUserData, pHead->nBlockUse );//把内存还给对应的自由链表
    
            __FINALLY
                _munlock(_HEAP_LOCK);  /* release other threads */
            __END_TRY_FINALLY
    
            return;
    }

    不调用析构函数的情况:类型为内置类型/未显式定义析构函数且编译器未合成默认析构函数。

  7. new[ ]以及delete[ ]:会多开辟四个字节(存放对象个数)以提示编译器自己该执行构造/析构函数n多次                                   new[ ]    

    void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc)
    	{	// try to allocate count bytes for an array
    	return (operator new(count));
    	}
    
    void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) //调用operator new的次数
            {       // try to allocate size bytes                     //编译器会多开辟一个字节来
            void *p;                                                  //存储对象个数
            while ((p = malloc(size)) == 0)
                    if (_callnewh(size) == 0)
                    {       // report no memory
                            _THROW_NCEE(_XSTD bad_alloc, );
                    }
    
            return (p);
            }

    delete[ ]                                                                                                                                                                                         

    void operator delete[]( void * p )
    {
        RTCCALLBACK(_RTC_Free_hook, (p, 0))
    
        operator delete(p);
    }
    

            以代码加内存的形式演示怎么多开辟了一个字节  

    #include<iostream>
    using namespace std;
    class A
    {
    public:
    	A(int _b=0)
    	:b(_b)
    	{
    		cout << "A()" << endl;
    	}
    	~A()
    	{
    		cout <<"~A()"<< endl;
    	}
    private:
    	int b;
    };
    
    int main()
    {
    	A* p = new A[5];
    	delete[] p;
    	system("pause");
    	return 0;
    }

       监视窗口:                                                  对象模型:                                                                                                                    

  8. 程序崩溃:1.0 new开辟空间delete[ ]释放它会先访问*( (int*)p-1)确定需要析构多少次,而此时它并没有存储对象数量所以访问非法内存导致程序错误(触发断点)。2.0 new[ ]用delete来释放空间(释放部分空间)3.0 new[ ]用free来释放空间(触发一个断点)

3.0 继承

  1. 继承实现代码复用。                                                                                                                                                                                                
  2. 访问限定符protected:基类成员在类外不能被访问,在派生类中可以被访问。
  3. 必须定义派生类构造函数的场景:1.0 基类没有缺省的构造函数在派生类的初始化列表中必须带上基类名和参数列表。2.0 基类有带参数列表的构造函数。
  4. 赋值兼容规则/作用域/同名隐藏:子类和基类属于不同的作用域,子类可以赋值给父类,父类的指针或引用可以指向子类。父类和子类拥有相同的成员名时父类的成员会被隐藏。
  5. 友缘关系不能被继承:基类的友缘函数不能访问子类的私有/保护成员。
  6. 基类的静态成员只有一份。
  7. 菱形继承:  B1公有继承自A,B2公有继承自A,C公有继承自B1和B2                                                                                                                                                                                                                                   存在的问题B1和B2中都有A的成员变量在C继承他们后再其模型内就会有两份A的数据 造成空间浪费,数据冗余。               所以有了菱形虚拟继承  基类A数据在C中只保留1份。                                                                                                             
    #include<iostream>
    using namespace std;
    class Base
    {
    public:
    	void FunTest1(){}
    public:
    	int _p1;
    };
    class C1 : virtual public Base
    {
    public:
    	void FunTest2(){}
    public:
    	int _p2;
    };
    class C2 :virtual public Base
    {
    public:
    	void FunTest3(){}
    public:
    	int _p3;
    };
    class D :public C1, public C2
    {
    public:
    	void FunTest3(){}
    public:
    	int _p4;
    };
    int main()
    {
    	D d;
    	d._p1 = 1;
    	d._p2 = 2;
    	d._p3 = 3;
    	d._p4 = 4;
    	system("pause");
    	return 0;
    }

                                        

  8. 不能继承的函数:友元函数 

4.0 多态

  1. 一种东西在不同情况下具有不同的形态。
  2. 静态多态:1.0 函数重载:参数列表不同/cv限定符 2.0 泛型编程(宏/模板)  算法的的通用性。【编译器在编译期间完成】
  3. 动态多态:继承+虚函数机制   动态绑定:在程序执行期间判断所引用对象的实际类型,调相应方法。     (派生类重写基类虚函数)         动态绑定条件: 1.0 通过基类的引用或者指针来调虚函数   2.0 必须是虚函数(派生类一定要重写基类的虚函数
  4. 纯虚函数:在虚函数的参数列表后加上=0,有虚函数的类叫抽象类不能实例化,派生类只有在将纯虚函数重写后才能实例化对象      来段代码体会下 动态绑定过程以及纯虚函数                                                                                                                                                        
    #include<iostream>
    #include<windows.h>
    using namespace std;
    
    class WashRoom {
    public:          
    	void GoToManWashRoom()         
    	{ 
    		cout << "Man-->Please Left" << endl;
    	}
       void GoToWomanWashRoom()          
       { 
    	   cout << "Woman-->Please Right" << endl;
       }
    };
    class Person 
    { 
    public:           
    	virtual void GoToWashRoom(WashRoom & _washRoom) = 0; 
    };
    class Man :public Person 
    {
    public:           
    	virtual void GoToWashRoom(WashRoom & washRoom)          
    	{ 
    		washRoom.GoToManWashRoom(); 
    	} 
    };
    class Woman :public Person 
    { public:           
    	virtual void GoToWashRoom(WashRoom & washRoom)          
    	{ 
    		washRoom.GoToWomanWashRoom(); 
    	} 
    };
    void FunTest() 
    {
    	WashRoom washRoom;           
    	for (int iIdx = 1; iIdx <= 10; ++iIdx)          
    	{
    		Person* pPerson;                    
    		int iPerson = rand() % iIdx;                    
    		if (iPerson & 0x01)                             
    			pPerson = new Man;                   
    		else                             
    			
    			pPerson = new Woman;
    		pPerson->GoToWashRoom(washRoom);                  
    		delete pPerson;                  
    		pPerson = NULL;                   
    		Sleep(1000);
    	}
    }
    int main()
    {
    	FunTest();
    	system("pause");
    	return 0;
    }

    结果:    

  5. 重载/重写/隐藏: 重载:同一个作用域,重写:不同作用域(基类/派生类)函数名/参数列表/返回值下相同(协变例外)访问修饰符不影响     隐藏:函数名相同(继承体系中)

  6. 哪些成员函数不能定义为虚函数:构造函数(构造函数在执行完之前对象都没有,不满足动态绑定条件基类的指针或者引用),友元函数(不满足成员函数的条件)静态成员函数(独立于对象之外,没有this指针所以不能定义为你虚函数)         赋值运算符重载(没必要,容易造成混乱)                                                                                                                            

    struct B {
    virtual B& operator= (const B&);
    };
    struct D : B {
    virtual D& operator= (const B&); //重写
    };
    D obj1, obj2;
    obj1 = obj2; //调用默认定义的D::operator=(const D&)

     

  7. 建议写成虚函数的成员函数:析构函数:Base* b1 = new Derived() ;delete b1;b1 = NULL; 不加virtual关键字只会调基类的析构函数加了后会多调用派生类的析构函数。

  8. 类外定义虚函数:声明时加上关键字virtual定义时不用。

  9. 不要在构造/析构函数体内执行虚函数:对象不完整,结果未定义。

  10. 类对象共享同一个虚表。

  11. 引入虚函数的单继承多继承菱形继承菱形虚拟继承。                                                                                                                 单继承:

    #include<iostream>
    using namespace std;
    class Base
    {
    public:
    	virtual void Test1()
    	{
    		cout << "Base::Test1()" << endl;
    	}
    	virtual void Test2()
    	{
    		cout << "Base::Test2()" << endl;
    	}
    	int _b;
    };
    class Derived :public Base
    {
    public:
    	virtual void Test1()
    	{
    		cout << "Derived::Test1()" << endl;
    	}
    	virtual void Test2()
    	{
    		cout << "Derived::Test2()" << endl;
    	}
    	int _d;
    };
    void Test()
    {
    	Base b;
    	Derived d;
    	b._b = 0;
    	d._b = 1;
    	d._d = 2;
    }
    int main()
    {
    	Test();
    	system("pause");
    	return 0;
    }
    
    

      对象模型:                                                                                     多继承:

    #include<iostream>
    using namespace std;
    class B1
    {
    public:
    	virtual void Test1()
    	{
    		cout << "B1::Test1()" << endl;
    	}
    	int _b1;
    };
    class B2
    {
    public:
    	virtual void Test2()
    	{
    		cout << "B2::Test2()" << endl;
    	}
    	int _b2;
    };
    class Derived :public B1, public B2
    {
    public:
    	virtual void Test1()
    	{
    		cout << "Derived::Test1()" << endl;
    	}
    	virtual void Test2()
    	{
    		cout << "Derived::Test2()" << endl;
    	}
    	virtual void Test3()
    	{
    		cout << "Derived::Test3()" << endl;
    	}
    	int _d;
    };
    void Test()
    {
    	Derived d;
    	d._b1 = 1;
    	d._b2 = 2;
    	d._d = 3;
    	B1& b1 = d;
    	B2& b2 = d;
    	cout << sizeof(b1) << endl;
    	cout << sizeof(b2) << endl;
    	cout << sizeof(d) << endl;
    }
    int main()
    {
    	Test();
    	system("pause");
    	return 0;
    }

          菱形继承: 

    #include<iostream>
    using namespace std;
    class Base
    {
    public:
    	virtual void FunTest1(){}
    public:
    	int _p1;
    };
    class C1 :public Base
    {
    public:
    	virtual void FunTest1(){
    		cout << "C1.FunTest1()" << endl;
    	}
    	virtual void FunTest2(){
    		cout << "C1.FunTest2()" << endl;
    	}
    public:
    	int _p2;
    };
    class C2 :public Base
    {
    public:
    	virtual void FunTest1(){
    		cout << "C2.FunTest1()" << endl;
    	}
    	virtual void FunTest3(){
    		cout << "C2.FunTest3()" << endl;
    	}
    public:
    	int _p3;
    };
    class D :public C1, public C2
    {
    public:
    	virtual void FunTest2()
    	{
    		cout << "D.TunTest2()" << endl;
    	}
    	virtual void FunTest3()
    	{
    		cout << "D.TunTest3()" << endl;
    	}
    public:
    	int _p4;
    };
    int main()
    {
    	D d;
    	C1& c1 = d;
    	C2& c2 = d;
    	d.C1::_p1 = 1;
    	d._p2 = 2;
    	d._p3 = 3;
    	d.C2::_p1 = 1;
    	d._p4 = 4;
    	cout << sizeof(c1) << endl;
    	cout << sizeof(c2) << endl;
    	cout << sizeof(d) << endl;
    	system("pause");
    	return 0;
    }

                                                                          菱形虚拟继承:       

    #include<iostream>
    using namespace std;
    class Base
    {
    public:
    	virtual void FunTest1(){}
    public:
    	int _p1;
    };
    class C1 :virtual public Base
    {
    public:
    	virtual void FunTest1(){
    		cout << "C1.FunTest1()" << endl;
    	}
    	virtual void FunTest2(){
    		cout << "C1.FunTest2()" << endl;
    	}
    public:
    	int _p2;
    };
    class C2 :virtual public Base
    {
    public:
    	virtual void FunTest1(){
    		cout << "C2.FunTest1()" << endl;
    	}
    	virtual void FunTest3(){
    		cout << "C2.FunTest3()" << endl;
    	}
    public:
    	int _p3;
    };
    class D :public C1, public C2
    {
    public:
    	virtual void FunTest1()  //D调用FunTest1时不明确是C1的还是c2的不知道。所以重写后保存在c1的虚表里。
    	{                        //菱形虚拟继承是为了解决菱形继承的问题(空间冗余所以基类在D中应该只有一份。
    		cout << "D.TunTest1()" << endl;
    	}
    	virtual void FunTest2()
    	{
    		cout << "D.TunTest2()" << endl;
    	}
    	virtual void FunTest3()
    	{
    		cout << "D.TunTest3()" << endl;
    	}
    	virtual void FunTest5()
    	{
    		cout << "D.TunTest5()" << endl;
    	}
    public:
    	int _p4;
    };
    int main()
    {
    	D d;
    	C1& c1 = d;
    	C2& c2 = d;
    	d._p1 = 1;
    	d._p2 = 2;
    	d._p3 = 3;
    	d._p4 = 4;
    	cout << sizeof(c1) << endl;
    	cout << sizeof(c2) << endl;
    	cout << sizeof(d) << endl;
    	system("pause");
    	return 0;
    }
    

                                     

  12. 虚函数调用过程:step1:判断对象类型(基类?派生类) step2:通过虚表指针_vfptr来找到虚表地址 step3:  从虚表里获取虚函数的地址  step4:找到并调用函数          *它的调用多了虚表指针多以有时间冗余*         

5.0 智能指针 

  1. 种类:std::auto_ptr/std::unique_ptr/std::shared_ptr/std::weak_ptr

  2. auto_ptr:独享对象所有权  注意:1.0 不能用两个auto_ptr指向同一个对象 2.0 它的析构函数用的是delete所以不要用auto_ptr开辟数组hedelete[ ]和new[ ]匹配 3.0 它的拷贝构造函数/赋值运算符重载函数内将原来的auto_ptr赋空所以不能将auto_ptr存在容器里 4.0 避免使用auto_ptr    

  3. unique_ptr:    独享对象所有权与boost::scoped_ptr原理相同只是多了些功能。把拷贝构造函数和符合运算符重载设置为private 并且只是申明不定义。多了->访问符和*解引用。

  4. share_ptr:共享对象所有权(引用计数Ref 浅拷贝)     存在的问题:循环引用                      双向链表  NULL--a---b---NULL 引用计数都为2 删除a前得删除b,删除b前得删除a。

  5. weak_ptr: 弥补shared_ptr引用计数的循环引用问题。shared_ptr对象里用了weak_ptr   它不会使得Ref++                               解决循环引用的示意图:                                                                                                                              

6.0 string类 

 1. 浅拷贝:             

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<cstring>
class String
{
public:
	String(const char* str = "")
		:_str(new char[strlen(str)+1])
	{
		strcpy(_str, str);
	}
	~String()
	{
		if (_str!=NULL)
		delete[] _str;
		_str = NULL;
	}
private:  char* _str;
};
void FunTest() 
{ 
  String s1;    
  String s2("bit-tech");    
  String s3(s2);    
  String s4;    
  s4 = s3; 
}
int main()
{
	FunTest();
	system("pause");
	return 0;
}​

                                                                                                                                               s2 s3 s4 指向同一块空间释放时不知道别的对象已经释放空间所以程序崩溃,一块空间被释放两次。                                            2. 深拷贝:               

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class String
{
public:
	String(const char* pStr = " ")
	{
		if (NULL == pStr)
		{
			_pStr = new char[1];
			*_pStr = '\0';
		}
		else
		{
			_pStr = new char[strlen(pStr) + 1];
			strcpy(_pStr, pStr);
		}
	}
	//拷贝构造函数一:
	String(const String& s) :_pStr(new char[strlen(s._pStr) + 1])
	{
		strcpy(_pStr, s._pStr);
	}
	//拷贝构造函数二:
	//String(const String& s)//缺点:_pStr指向不明,野指针,非手动开辟,手动释放,出错。
	//{
	//	if (NULL != _pStr)     不能直接delete这段空间不明确,不是自己手动new的delete会出问题
	//		delete[] _pStr;
	//	_pStr = new char[strlen(s._pStr) + 1];
	//	strcpy(_pStr, s._pStr);
	//}
	//赋值运算符重载:   1.0申请新空间,拷贝元素
	//                   2.0释放旧空间
	//                   3.0使用新空间(更改指向)
	//赋值运算符重载一:
	String& operator=(const String& s)
	{
		if (this != &s)   //防止自赋值
		{
			char* pTemp = new char[strlen(s._pStr) + 1];
			strcpy(pTemp, s._pStr);
			delete[] _pStr;
			_pStr = pTemp;
		}
		else
			return *this;
	}
	//赋值运算符重载二:
	/*String& operator=(const String& s)   //缺点:直接释放空间,万一下一步开辟失败
	{                                      //也找不到原来指向的空间。
	if (this != &s)
	{
	delete[] _pStr;
	_pStr = new char[strlen(s._pStr) + 1];
	strcpy(_pStr, s._pStr);
	}
	else
	return *this;
	}*/
	~String()
	{
		if (_pStr)
		{
			delete[] _pStr;
			_pStr = NULL;
		}
	}
private:
	char* _pStr;
};
void Test()
{
	String s1("hello");
	String s2;
	String s3(NULL);
	String s4(s3);
}
int main()
{
	Test();
	system("pause");
	return 0;
}

                             

需要注意:赋值的自赋值,拷贝构造的先开辟空间后元素搬移,赋值的先开辟空间搬移元素再释放就空间最后更改指向。

3. 写实拷贝:          3种 浅拷贝+mutable int   浅拷贝+static int     浅拷贝+ int*          

  只有修改数据时才会触发写实拷贝                                                                

 浅拷贝+mutable int

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class String
{
public:
	String(const char* pStr = " ")
	{
		if (NULL == pStr)
		{
			_pStr = new char[1];
			*_pStr = '\0';
		}
		else
		{
			_pStr = new char[strlen(pStr) + 1];
			strcpy(_pStr, pStr);
		}
		_count = 1;
	}
	String(const String& s) :_pStr(s._pStr), _count(++s._count)  //同一块空间的拥有者个数都一样
	{}
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			String StrTemp(s._pStr);
			swap(_pStr, StrTemp._pStr);
			swap(_count, StrTemp._count);
		}
		return *this;
	}
	~String()
	{
		if (_pStr&&--_count == 0)  //每次执行析构函数空间的拥有者count都会-1直到-1之后为0才可以真正析构
		{
			delete[] _pStr;
			_pStr = NULL;
		}
	}
private:
	char* _pStr;
	mutable int _count;      //mutable  是因为拷贝构造函数的参数const String& s 要改变一个const对象的内容
};

void Test()
{
	String s1("hello");
	String s2;
	String s3;
	s3 = s1;
	String s4(s3);
}
int main()
{
	Test();
	system("pause");
	return 0;
}

       以上代码存在问题: s4 s3共用同一块空间   析构 s4时 --count 可是s3不知道count已经减减所以它的count依旧为2 ,同一           块空间会被销毁两次。

     浅拷贝+ static int

      

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class String
{
public:
	String(const char* pStr = " ")
	{
		if (NULL == pStr)
		{
			_pStr = new char[1];
			*_pStr = '\0';
		}
		else
		{
			_pStr = new char[strlen(pStr) + 1];
			strcpy(_pStr, pStr);
		}
		_count = 1;
	}
	String(const String& s) :_pStr(s._pStr)  //同一块空间的拥有者个数都一样
	{
		_count = s._count;
		++_count;
	}
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			String StrTemp(s._pStr);
			swap(_pStr, StrTemp._pStr);
			swap(_count, StrTemp._count);
		}
		return *this;
	}
	~String()
	{
		if (_pStr&&--_count == 0)  //每次执行析构函数空间的拥有者count都会-1直到-1之后为0才可以真正析构
		{
			delete[] _pStr;
			_pStr = NULL;
		}
	}
private:
	char* _pStr;
	static int _count;      
};
int String::_count = 0;
void Test()
{
	String s1("hello");
	String s2;
	String s3;
	s3 = s1;
	String s4(s3);
}
int main()
{
	Test();
	system("pause");
	return 0;
}

  

只要用构造函数创建出新对象String::_count就会被置为1 不满足需求。

需要考虑: static变量的类外定义,static型变量为类所有。

浅拷贝+ int*

 

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class String
{
public:
	String(const char* pStr = " ")
		:_count(new int[1])
	{
		if (NULL == pStr)
		{
			_pStr = new char[1];
			*_pStr = '\0';
		}
		else
		{
			_pStr = new char[strlen(pStr) + 1];
			strcpy(_pStr, pStr);
		}
		*_count = 1;
	}
	String(const String& s) :_pStr(s._pStr)  //同一块空间的拥有者个数都一样
	{
		_count = s._count;
		++(*_count);
	}
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			if (_pStr&&--(*_count)==0)   //赋值也需要判断是否delete原来的_pStr
			{                            //_pStr指向的空间非空以及引用计数为1则需要把源空间释放
				delete[] _pStr;
				delete[] _count;
			}
			_pStr = s._pStr;
			++(*s._count);
			_count = s._count;
		}
		return *this;
	}
	~String()
	{
		if (_pStr&&--(*_count) == 0)  //每次执行析构函数空间的拥有者count都会-1直到-1之后为0才可以真正析构
		{
			delete[] _pStr;
			_pStr = NULL;
		}
	}
private:
	char* _pStr;
	int* _count;
};
void Test()
{
	String s1("hello");
	String s2;
	String s3;
	s3 = s1;
	String s4(s3);
}
int main()
{
	Test();
	system("pause");
	return 0;
}

      缺点:维护两块空间。

   4. new型 浅拷贝

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class String
{
public:
	String(const char* pStr = " ")
	{
		if (NULL == pStr)
		{
			_pStr = new char[1 + 4];        //多开辟四个字节操作时_pStr先偏移4个字节
			_pStr += 4;
			*_pStr = '\0';
		}
		else
		{
			_pStr = new char[strlen(pStr) + 1 + 4];
			_pStr += 4;                       //操作时_pStr先偏移4个字节
			strcpy(_pStr, pStr);
		}
		GetRef() = 1;
	}
	String(const String& s) :_pStr(s._pStr)
	{
		GetRef()++;
	}
	String& operator=(const String& s)
	{
		if (_pStr != s._pStr)   //避免:String s2(s1); String; s2=s1;的情况发生。
		{
			Release();
			_pStr = s._pStr;
			++GetRef();
		}
		return *this;
	}
	char& operator[](size_t index)
	{
		if(GetRef() > 1)               //要对空间_pStr进行改变时写实拷贝(引用计数>=2)
		{ 
			char* pTemp = new char[strlen(_pStr) + 1 + 4];   //1.0 开辟临时空间  (多4个字节) 
			*(int*)pTemp = 1;                                //2.0 初始化引用计数为1
			pTemp += 4;                                      //3.0 挪移元素(从偏移量为4的字节数开始)
			strcpy(pTemp, _pStr);
			--GetRef();                                     // 4.0 源引用计数减减
			_pStr = pTemp;                                  // 5.0 更改指向
		}
		return _pStr[index];          //引用计数==1直接返回index对应的_pStr[index]可以直接修改          
	}
	const char& operator[](size_t index) const     //不改变直接返回index对应的_pStr[index]
	{
		return _pStr[index];
	}
	friend ostream& operator<<(ostream& _cout, const String& c);
	~String()
	{
		Release();
	}
private:
	char* _pStr;
	int& GetRef()
	{
		return *((int*)_pStr - 1);     //回退4个字节(空间开始为堆的低地址)可能改变其值所以传引用
	}
	void Release()
	{
		if (_pStr && 0 == --GetRef())
		{
			_pStr -= 4;         //多开辟的4个字节也得释放
			delete[] _pStr;
			_pStr = NULL;
		}
	}
};
ostream& operator<<(ostream& _cout, const String& c)
{
	_cout << c._pStr;
	return _cout;
}
void Test()
{
	String s1("hello");
	String s2(s1);
	String s3;
	String s4(s3);
	s3 = s2;
	s1[2] = 'o';
	cout << s1 << endl;
}
int main()
{
	Test();
	system("pause");
	return 0;
}

         

核心在于模仿new多开辟4个字节用来保存引用计数操作从偏移量为4位置开始_pStr+=4;   改变_pStr的情况有[ ] 运算符重载内部需要分情况引用计数大于1 时1.0 开辟临时空间  (多4个字节。2.0 初始化引用计数为1。 3.0 挪移元素(从偏移量为4的字节数开始) 4.0 源引用计数减减。 5.0 更改指向。 引用计数等于1时  接返回index对应的_pStr[index]   析构时注意把_pStr后移4字节和引用计数空间一起释放    访问前4个字节:*((int*)_pStr-1) 

7.0 类型转换

  1. C语言强制/隐式类型转换格式单一错误不好跟踪。C++的类型转换分为4种:static_cast/ reinterpret_cast/ const_cast/ dynamic_cast
  2. static_cast和dynamic:static_cast静态转换即非多态类型转换,隐式类型转换都可以用它完成。int i=1; double a=static_cast<double> (i);  使用场景:1. 0 继承中向上转换 2.0 把任意类型转换为void型                                                       dynamic主要在运行时转换在多态种存在 。                                                                                                                            向上转换(子类的指针/引用-->父类的指针/引用)不需要   向下转换(父类的指针/引用-->子类的指针/引用)          不安全(本质上是派生类的安全,本实质上是基类的不安全)                                                                                                         1.0 dynamic_cast只能用于有虚函数的类 2.0 会先检查能否转换成功可以就转换不可以就返回0                                             
    #include<iostream>
    using namespace std;
    class A 
    { 
    public:     
    	virtual void f()
    	{} 
    };
    class B : public A 
    {};
    void fun(A* pa) 
    {   
    	// dynamic_cast会先检查是否能转换成功,能成功则转换,不 能则返回
    	B* pb1 = static_cast<B*>(pa);
    	B* pb2 = dynamic_cast<B*>(pa);  
    	cout << "pb1:" << pb1 << endl;
    	cout << "pb2:" << pb2 << endl;
    }
    int main()
    { A a;    
     B b;     
     fun(&a);     
     fun(&b);
     system("pause");
     return 0; 
    }
                                                                                                                                                                                                                                                                                                           
  3.   const_cast: 去const属性。       
    void Test () 
    {     
      const int a = 2;
      int* p = const_cast< int*>(&a );    
      *p = 3;
      cout<<a <<endl; 
    }

     

  4.  reinterpreter_cast: 处理互不相关类型之间的转换,double* b=reinterpret_cast<double*>(a); //b的转换结果为0x0000000a    int--int*--int 最后转换成int时可以和之前的一样但是机器可能会忘记前值所以reinterpreter_cast依赖机器
  5.  去const属性用const_cast。 基本类型转换用static_cast。 多态类之间的类型转换用daynamic_cast。 不同类型的指针类型转换用reinterpreter_cast。
  6. typeid(变量名).name() 求变量类型名  C++11的auto可以进行变量类型自动推导,返回值占位  auto FUN(return double())

8.0 编译与链接

  1.  程序运行分为 预处理---编译---汇编---链接。
  2.  预处理:宏替换/保留所有的#pragma编译器指令/添加行号和文件名标识/删除注释/处理条件编译/处理#include预编译指令
  3. 编译:对预处理后的文件进行词法/语法/语义分析产生相应的中间/目标代码    词法分析:字符分割  语法分析:产生语树      语义分析:类型检查,生成中间代码/目标代码。  目标文件包含了:符号表/调试信息/字符串(链接的调试信息)
  4. 汇编:汇编代码---机器代码 
  5. 链接:未解决的符号表:列出本单元有引用但定义不在本单元的符号及地址(需要什么)。  导出符号表:本单元定义的一些符号和地址的映射表(有什么)。  地址重定向表:提供了本编译单元所有对自身地址引用的记录(导出符号表里的符号号地址最后都会加上可执行文件的定位地址)造成与原来导出符号表映射关系不符合多以有了地址重定向表保存符号的新地址。    链接器工作过程:1.0 决定各个目标文件在可执行文件的位置 2.0 访问所有的地址重定向表对其地址进行修改(加上其目标文件在可执行文件的起始地址)3.0 遍历未解决符号表,在导出符号表里查找匹配符号,找到了就在未解决符号表对应符号的地址处添加匹配符号的地址。 4.0 最后把目标文件的内容写到各自的位置再做一些工作生成了可执行文件。
  6. extern:外部链接:将该符号放置在未解决符号表里。
  7. 函数/变量默认外部链接,const型变量默认内部链接   外部链接:错误 duplicated external simbols  重复外部符号                内部链接:不同的编译单元可以具有相同的内部链接符号。
  8. 动态编译:可执行文件带一个动态库(大)可执行文件小,移植性差。 静态态编译:将可执行文件需要的那部分动态库(.so)中的部分取出来链接到可执行文件中去。运行时不依赖于动态库,可是体积大。

9.0 类型萃取

  1. 萃取使用场景分离出内置类型和自定义类型。
  2. #include<iostream>
    #include<string>
    using namespace std;
    struct __TrueType
    {
    	bool Get()    
    	{ 
    		return true;
    	} 
    };
    struct __FalseType 
    { 
    	bool Get()    
    	{ 
    		return false; 
    	}
    };
    template <class _Tp> 
    struct TypeTraits 
    { 
    typedef __FalseType   __IsPODType;   //自定义类型设置为_FalseType
    };
    //偏特化
    template <> struct TypeTraits< bool> { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< char> {typedef __TrueType     __IsPODType;};
    template <> struct TypeTraits< unsigned char > { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< short> { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< unsigned short > { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< int> { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< unsigned int > { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< long> { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< unsigned long > { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< long long > { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< unsigned long long> { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< float> { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< double> { typedef __TrueType     __IsPODType; };
    template <> struct TypeTraits< long double > { typedef __TrueType     __IsPODType; };
    template <class _Tp>
    struct TypeTraits< _Tp*> 
    {
    	typedef __TrueType     __IsPODType;
    };
    // // 使用参数推导的萃取处理 //
    template <class T> 
    void Copy (const T* src , T* dst, size_t size, __FalseType ) 
    {     
    	cout<<"__FalseType:" <<typeid( T).name ()<<endl;     
    	for (size_t i = 0; i < size ; ++i)    
    	{        
    		dst[i ] = src[ i];   
    	}
    }
    template <class T> 
    void Copy(const T* src, T* dst, size_t size, __TrueType)
    {
    	cout << "__TrueType:" << typeid(T).name() << endl;
    	memcpy(dst, src, size*sizeof (T));
    	// // 使用萃取判断类型的Get函数判断是否是 POD类型来处理 
    	if (TypeTraits <T>::__IsPODType().Get())
    	{
    		memcpy(dst, src, size*sizeof (T));    //内置类型用memcpy高效
    	}
    	else
    	{
    		for (size_t i = 0; i < size; ++i)
    		{
    			dst[i] = src[i];
    		}
    	}
    }
    void Test1() 
    {
    	string s1[10] = { "1", "2", "3", "4444444444444444444444444" };     
    	string s2[10] = { "11", "22", "33" };     
    	Copy(s1, s2, 10, TypeTraits <string>::__IsPODType());     
    	int a1[10] = { 1, 2, 3 };     
    	int a2[10] = { 0 };     
    	Copy(a1, a2, 10, TypeTraits <int>::__IsPODType());     
    }
    int main()
    {
    	Test1();
    	system("pause");
    	return 0;
    }
    

10.0 模板&泛型编程

  1. 编写通用函数:函数重载?弊端:1.0 新类型出现立即添加对应函数 2.0 只是参数列表不同函数体完全相同代码复用率不高 3.0 如果只是返回值不同不能识别(重载) 4.0 一个方法出错全都出错。 继承?所有的通用代码都放置在基类里 弊端:不会进行类型检查/代码维护困难  宏函数? 安全性不高 不进行参数检测 。泛型编程编写通用方法模板为基础。
  2. 函数模板:与类型无关,在使用时根据实参的类型产生相应的函数版本,模板实例化:编译器通过模板产生相应的类或者函数特定类型的版本。
  3. 模板编译:两次,第一次检测语法是否错误(分号是否遗漏?)第二次检查代码查看调用是否正确。
  4. 参数推演:从实参确定模板参数类型的过程。 
  5. 非模板类型参数转换:编译器只会执行两种转化                                                                                                                    1.0 const转换:模板为const 类型 传参为类型    template<const int *a> class A{}; int b; A<&b> m;                                2.0 函数或者数组的转换:函数转换为函数指针,数组转换为指向第一个元素的指针。                                                          template <int *a> class A{}; int b[1]; A<b> m;
  6. 模板参数作用范围:在模板形参之后到模板声明或定义的末尾之间 。遵循名字屏蔽规则。
  7. 注意: 1.0 模板参数的名字在模板参数列表里只出现一次。2.0 所有的模板形参前面都要加上class/typename关键字修饰。3.0 函数模板内部不能指定缺省值。
  8. 编译器也偷懒:在模板函数与非模板函数完全一致时,编译器不会合成这种模板函数,会自动调用非模板函数。
  9. 显示制定一个空的模板参数列表即< > 告诉编译器这个函数的调用智能用模板函数。所有模板参数由实参推演出来。
  10. 模板函数特化:规则:1.0 template<>  2.0 函数名<特化后的类型> 3.0 必须要和基础模板函数的参数类型一致 。
  11. 模板类:  template <class T, class Container = SeqList<T> > // 缺省参数  container也是一个T类型的模板类。                                      template<class T,template<class> class Container=SeqList>  等价于上面
  12. 浮点数和类对象是不允许作为非类型模板参数的。
  13. 特化后定义成员函数不再需要模板形参。 模板的特化(全/偏)都是在已经定义的模板基础之上,不能单独存在。
  14. 模板的分离编译:.h申明.cpp定义.cpp测试(实例化模板参数)测试文件只包含.h会造成实例化失败,无法走到定义部分。将模板的声明和定义放置在同一个文件里即可。
  15. 模板会让错误信息冗长,无法定位,代码编译时间变长。但是提高了代码的复用性。
  16.  当一个模板不被用到时就不会被实例化    分离式编译:编译某一个.pp文件时不知道其他.cpp文件的存在。找不到它的定义时会把它放在未解决符号表内,实现该模板的.cpp文件中并没有用到模板的实例编译器不会实例化,整个工程找不到模板实例化的二进制代码。所以报错:链接错误。

11.0 异常处理

  1. 程序消亡3种方式:自杀 他杀 无疾而终
  2. C语言异常处理: 1.0 暴力解决abort()和exit()2.0 返回一个合法值程序异常atoi 不能转化时返回0 3.0 goto 4.0 回调异常处理函数(自己提前设置的) 5.0 程序终止(分母为0) 6.0 返回错误码打印错误信息(GetLastError())                           7.0 setjmp+longjmp: int setjmp(jmp_buf envbuf ) 缓冲区envbuf用来保存堆栈内容                                                                         void longjmp(jmp_buf envbuf int val) envbuf缓冲区是setjmp保存堆栈环境的 val用来设置setjmp的返回值。先调用 setjmp记录当前堆栈位置再调用longjmp返回envbuf记录的位置 设置setjmp的返回值                                                               
    #include<iostream>
    using namespace std;
    #include<setjmp.h>
    
    jmp_buf buf;//定义全局变量buf,保存当前信息的执行点
    void FunTest1()
    {
        longjmp(buf, 1);
    }
    
    void FunTest2()
    {
        longjmp(buf, 2);
    }
    
    void FunTest3()
    {
        longjmp(buf, 3);
    }
    
    int main()
    {
        int iState = setjmp(buf);
        if(iState == 0)
        {
            FunTest1(); //一旦longjmp()函数跳到执行点后,后面的将不执行
            FunTest2();
            FunTest3();
        }
        else
        {
            switch(iState)
            {
            case 1:
                cout<<"FunTest1()"<<endl;//验证setjmp()和longjmp()
                break;
            case 2:
                cout<<"FunTest2()"<<endl;
                break;
            case 3:
                cout<<"FunTest3()"<<endl;
                break;
            }
        }
        system("pause");//打印FunTest1()
        return 0;
    }

                                                                                                                                                                           注意:setjmp必须在longjmp之前执行,在setjmp返回之前调用longjmp设置其返回值。

  3. C++异常处理:异常:函数发现无法处理的错误时让函数调用者直接或者间接处理该错误。                                                  异常的抛出与捕获:三个关键字 try标识可能出现异常的代码段   catch标识处理异常的代码段  throw抛出一个异常               throw包含在try里,throw 后面跟的值类型决定catch的参数类型    1.0 异常通常是由抛出的对象引发,该对象类型决定了该调用哪个异常处理函数。 2.0 被选中的处理函数是调用过程中对象类型匹配且离抛出异常位置最近的那一个。 3.0 抛出异常后会释放局部存储对象,所以抛出的对象也就被释放了,throw会初始化一个匿名(异常)对象副本,该匿名对象由编译器管理,直到把它传递给catch处理之后撤销。

  4. 异常处理机制:1.0 程序遇到异常后应该立即停止当前函数运行,查找匹配的catch语句 2.0 先检查throw是否在try语句内部是的话再查找catch语句 3.0 有匹配的处理没有的话继续在调用函数的栈中查找 4.0 直到到达main函数的栈依旧不匹配终止程序。沿着函数栈查找catch子句的过程:栈展开 

  5. 异常捕获规则:1.0 异常抛出对象的类型与catch的对象完全一致,但也有例外:1.0 抛出对象为派生类 catch对象为基类      2.0 抛出对象为非const类型 catch对象为const类型 3.0 函数/数组到指向第一个元素的指针和函数指针的转换。

  6. 异常的重新抛出:catch不能处理一个异常,就抛给上一层调用链处理。C++标准要求:异常对象必须要能被拷贝构造  考虑到效率所以catch的对象为引用类型,异常对象可能被修改。导致下一次的异常对象与上次的有所不同。try和catch嵌套                      

    #include<iostream>
    #include<string>
    #include<exception>
    using namespace std;
    int main()
    {
    	try       //二级异常捕获处理
    	{
    		try   //一级异常捕获处理
    		{
    			exception e("It's my error");
    			cout << "I'm trying" << endl;
    			throw e;
    		}
    		catch (exception &e)   
    		{
    			cout << "I'm First Exception but I can't handle it" << endl;
    			throw;
    		}
    	}
    	catch (exception &e)
    	{
    		cout << "I'm Second Exception if you can't handle it let me handle it" << endl;
    	}
    	system("pause");
    	return 0;
    }

        

  7. 异常的规范: 在函数申明之后列出可能抛出的异常类型并保证不会有其他类型的异常。 1.0 成员函数在申明和定义需要相同的异常规范。  2.0 函数抛出一个没有被列在它异常规范中的异常时(且函数中抛出异常没有在函数内部进行处理), 系统调用C++标准库中定义的函数unexpected().    3.0 派生类的虚函数的异常规范必须与基类虚函数的异常规范一样。 4.0 如果异常规范为throw(),则表示不得抛出任何异常,该函数不用放在try块中。

  8. 异常的构造与析构函数: 不要再构造函数内抛出异常,造成对象没有构造/初始化完整。 不要在析构函数内抛出异常可能会造成内存泄漏。 

  9. C++标准库定义的exception类&自定义异常类:                                                                                                                                                                                                                               

  10. 怎么自定义一个异常类: 重载/继承exception来实现自己的异常类。                                                                                       

    #include <iostream>
    #include <exception>
    using namespace std;
    struct MyException : public exception
    { 
    	const char * what () 
        const throw () 
    	{ 
    		return "C++ Exception"; 
    	}
    }; 
    int main()
    { 
    	try 
    	{ 
    		throw MyException();
    	} 
    	catch(MyException& e) 
    	{
    		std::cout << "MyException caught" << std::endl; 
    		std::cout << e.what() << std::endl; 
    	} 
    	catch(std::exception& e) 
    	{ 
    		//其他的错误 
    	}
    	system("pause");
    	return 0;
    }

                     

12.0 IO         

  1. C++ IO流:C++流类库重定义了cin cout cerr/clog标准错误输出流。   cin为缓冲流,输入的数据与提取的数据类型一致,出错的话流的状态字state对应位置为1程序继续 .   空格和回车都作为输入数据分隔符,多个数据可以在一行输入也可以在多行处输入如果提取类型为字符型和字符串空格无法用cin输入,字符中不能出现空格,回车符也无法读入。

  2. 文件操作:二进制文件/文本文件   字节/字符为信息单位   1.0定义一个文件流对象:    ifstream ifile(只输入用)    ofstream ofile(只输出用)    fstream iofile(既输入又输出用 2.0 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件 之间建立联系  3.0 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写  4.0 关闭文件                                                                                                                     

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值