c++-面向对象

概念

  • 成员变量:属性
  • 成员函数:方法

类在编译后不占内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据
只有在创建对象后才会给成员变量分配内存,并赋值

可以将类看做一种数据类型

补充 多文件

类的内部包含成员变量和成员函数,成员变量要等到具体的对象被创建时才会被定义(分配空间)但是成员函数需要在一开始就被定义,也就是类的实现
成员函数实现可以写在类定义的内部,然后放在头文件中
成员函数的定义不再类定义内部实现的情况下,不能早头文件中定义

类定义/实例化

class 类定义后面有一个分号,要注意!!!
struct 结构体定义后面也有一个分号

类可以定义在函数外,和函数内(通常很少定义在函数内)

对象创建的两个阶段:分配内存空间,再进行初始化
初始化才是调用构造函数
对于返回值是结构体和类对象时(值传递),为了防止局部对象被销毁,也为了防止通过返回值修改原来的局部对象,编译器并不会返回这个对象,而是根据这个对象先创建一个临时对象(匿名对象)以拷贝的方式进行,然后再将这个临时对象返回

class Student
{
public:
    char *name;
    int age;

    void say();
    // 定义
    // void say(){cout<<"hello"<<endl;}
};

实例化

Student stu1; // 等价于class Student stu1

// 还可以写成
Student stu1=Student();

互相赋值!!!

相同成员下的对象可以赋值

Student stu1,stu2;
stu1=stu2;

实例对象数组

对象数组,数组中每个元素都是一个类的实例对象

class ClsName
{
    dataType data;
};

ClsName instanceItems[Size]; //创建数组

对象指针/地址/new

注意

clsType *obj 并不会调用构造函数,只是定义了一个指向对象的指针,没有实际开辟内存

在栈上创建实例对象

class_name *ptr=&instance ptr指向一个class_name类型的数据(实例对象)

class Student
{
    char *name;
    int age;
};

int main()
{
    Student stu; // 在栈上创建分配对象的内存 Student Stu=Student()

    // 地址,&stu;
    Student *pStu=&stu;
}

在堆上创建实例对象

class_name *ptr=new class_name
dataType *ptr=new dataType
https://blog.csdn.net/baidu_31437863/article/details/86558339

int *p=new int[10]; // 因为实际数组的类型是int[10]
delete[] p;
Student *pStu=new Student; 
// Student *pStu;
// pStu=new Student;
delete pstu;

栈/堆创建对象的区别

栈上创建有名字,指针不必要
堆上创建只能通过指针创建

例子补充

String::String(const char *s)
{
	len=strlen(s);
	str=new char[len+1];
	strcpy(str,s);
}

在这里插入图片描述

成员与访问

栈上访问

instance.property/method(); // 进行访问

堆上访问

instance->property/method(); // 进行访问

public 公有

在类内可访问
类外 通过实例对象访问

公有方法可以访问公有属性
公有方法可以访问私有属性
公有方法可以调用公有方法
公有方法可以调用私有方法

实例对象可以访问公有属性
实例对象可以访问公有方法

类名可以直接访问公有属性、公有访问

修改公有属性,对多个实例对象之间没有影响

private 私有

在类内可访问,可通过对象的指针进行访问
在类外 不能通过实例对象 直接 访问

私有方法可以访问公有属性
私有方法可以访问私有属性
私有方法可以调用公有方法
私有方法可以调用私有方法

实例对象不可访问私有属性,(可以访问公有属性)
实例对象不可访问私有方法,(可以访问公有方法)

类名不可以直接访问私有属性、私有方法

private/public

注意,如果不写public,也不写private,就默认为private
一个类体里,private、public可以分别出现多次

class Student
{
private:
    char *m_name;
    int m_age;

public:
    void func(char *name);
    void func2(int age);
};

void Student::func(char *name)
{
    m_name=name;
}

void Student::func2(int age)
{
    m_age=age;
}

int main()
{
    // 实例化 
    Student stu;
    stu.func("zhangsan"); // 这个尽量不要写,因为这是一个常量,赋给了一个变量
    stu.func2(12);
    return 0;
}

例子2

Base::Base(const Base &instance)
{
    /*
        这里虽然是私有成员,但是拷贝构造函数传入来一个实例对象的引用相当于*this
        那么就相当于在类内访问私有成员,知道作用域在哪
    */ 
    this->a=instance.a;
    this->b=instance.b;
    this->count++;
    this->time=std::time((time_t *)NULL);
}

protected 保护

在类外不能通过对象访问,但是它在的派生类的内部可以访问

protected成员在本类中不能通过对象访问,实际上是不能在类外使用,在类内能使用
当存在继承关系时

基类中的protected成员可以在派生类中使用,(这个是相对于private的继承来说的)

成员函数 – 注意事项

c++可以在类的声明中,也可以在函数定义中声明缺省参数,但不能既在类声明中又在函数定义中同时声明缺省参数。因此,将定义或声明中的任意一个缺省参数删除即可

类的实例对象的内存模型

类的声明与定义不占用内存空间
类对象占用内存空间

编译器将成员变量、成员函数分开存储:分别对每个实例对象的成员变量分配内存,但是所有对象都共享同一段函数代码
成员变量在堆、栈区份分配内存
成员函数在代码区分配内存

sizeof(class_name); // 一般只算成员变量,注意内存对齐

类进阶

构造函数

构造函数名与类名相同 没有返回值,无需显示调用
在创建对象时自动执行
构造函数必须是public属性,否则创建对象时无法调用
不允许出现void返回值,函数体不能有return

如果用户没有自己定义构造函数,编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空,没有形参,也不执行任何操作,Student::Student(){}
一个类必须有构造函数,要么自己定义,要么编译器自动生成,用户定义后,编译器不会再自动生成(只要用户定义了构造函数,无论是无参还是有参构造,编译器都不在自动生成默认构造函数)

构造函数定义

class Student
{
public:
    Student(char *name,int age); // 声明构造函数
private:
    char *m_name;
    int m_age;
};

Student::Student(const char *name,int age)
{
    m_name=name;
    m_age=age;
}

int main()
{
    // 两种初始化方式
    // 栈上创建
    Student stu1("张三",1);
    // 还可以写成
    Student stu1=Student("张三",1); // 实际上是构造函数得到变量再通过移动构造实现的

    // 堆上创建
    Student *stu2=new Student("lisi",2); // 这只是普通的构造函数
    delete stu2;
}

默认构造函数

无参构造函数的调用

无参构造函数就是默认构造函数

class Student
{
public:
    Student();
};

// 无参数构造函数
Student::Student()
{
    /*code*/
}

int main()
{
    // 四种初始化方式
    // 栈上创建
    Student stu1;
    Student stu1();
    Student stu1=Student();

    // 堆上创建
    Student *stu2=new Student();
    Student *stu2=new Student;
    delete stu2;
}

缺省构造函数

注意:构造函数声明中定义了缺省参数,函数定义中就不能在写默认参数了

class MyClass
{
public:
    void func(int a=1);
}
// MyClass::func(int a=1){} // 这么写是错误的
MyClass::func(int a/*=1*/){} // 可以这么写

注意2:对于全缺省的构造函数,也叫做默认构造函数;无参数的默认参数也叫做默认构造函数
所以,全缺省的构造函数和无参数的构造函数不能同时被用户自定义在进行无参实例化的时候,编译器不知道要调用哪个默认构造函数

class Student
{
public:
    // 同时出现下面两个是错误的
    Student();
    Student(char *name="zhangsan",int age=10); 
};

int main()
{
    Student stu1; // 这种情况下不知道调用哪个默认构造函数
    return 0;
}

构造函数重载

构造函数可以重载
就是同名函数的不同参数列表、不同参数、不同参数顺序、不同参数类型
参数名不同不可以;只有缺省或不缺省不可以

// 下面两种算一种方法,同时出现会报重定义的错误
void MyClass(int a);
void MyClass(int a=1);  

构造函数初始化列表

https://blog.csdn.net/gx714433461/article/details/124285721
除了上述的初始化方法外,还可以使用初始化列表的方式对成员变量初始化
对象初始化列表要优先于当前对象的构造函数
注意:使用初始化列表,列表中的赋值语句顺序与类定义中的声明顺序无关,(所以实际编程中最好就是注意赋值顺序)

class Student
{
public:
    Student(char *name,int age); // 声明构造函数
private:
    char *m_name;
    int m_age;
};

// 普通赋值构造函数
Student::Student(const char *name,int age)
{
    m_name=name;
    m_age=age;
}

// 利用初始化列表实现构造函数
Student::Student(const char *name,int age)
    :m_name(name),
     m_age(age) // 与上面的方法等价
{
}

int main()
{
    // 两种初始化方式
    // 栈上创建
    Student stu1("张三",1);

    // 堆上创建
    Student *stu2=new Student("lisi",2);
    delete stu2;
}
// 注意的例子
Student::Student(const char *name,int age)
    :m_age(age),m_name(name)
{
    // 依然执行的顺序是
    // 所以当赋值有关联的时候,要注意顺序问题
    // 因为私有变量先声明m_name,后声明m_age,所以,使用初始化列表时,也会先执行m_name(name),后执行m_age(age)
    m_name=name;
    m_age=age;
}

初始化列表其他注意事项

  1. 初始化列表只能初始化一次,多次初始化会报错
  2. 初始化列表与构造函数内赋初值可以混合使用,且混合使用时多次赋值不冲突
  3. const成员变量必须使用初始化列表进行初始化
  4. 引用成员变量碧玺使用初始化列表进行出初始化(因为引用变量必须在定义时初始化)
  5. 没有默认构造函数的自定义类型成员变量,必须在初始化列表中进行初始化
class A
{
public:
	A(int x):x(x); // 没有默认构造函数
private:
	int x
};

class B
{
public:
	B(int val);
private:
	A _a;
	int &_val;
};

B::B()
	: _val(val)
	, _a(20)
{
	
}

const 成员变量的初始化

类在编译后不占内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据
只有在创建对象后才会给成员变量分配内存,并赋值
const 成员变量的初始化,必须使用初始化列表的方法

class Student
{
private:
    const int m_len;
public:
    Student(int len);
};

Student::Student(int len)
    :m_len(len) // 或者直接写 m_len(10)
{
    // 不能直接写成
    // m_len=len
}

Student::Student(*args,**kwargs)
    :m_len(10)
{
    // 不能直接写成
    // m_len=len
}
c11标准特性

从例子来看,对于隐式转换(列表初始化)来说,其实调用的是构造函数(与构造函数的初始化列表没有关系)
注意,只有单参数的可以直接使用clsType=val,对于多参数的类,只能使用clsType={val1,val2}

#include <iostream>
#include <string>

using namespace std;

class Student
{
private:
    string name;
    int age;
public:
    Student(string name);
    Student(string name,int age);
    void print();
};

Student::Student(string name)
    :name(name),age(10)
{
    // this->age=10;
    cout<<"单参数构造函数"<<endl;
}

Student::Student(string name,int age)
    :name(name),age(age)
{
    // this->age=age;
    // this->name=name;
    cout<<"多参数构造函数"<<endl;
}


void Student::print()
{
    cout<<this->name<<" "<<this->age<<endl;
}

int main()
{
    Student s1={"li"}; // 单参数构造函数
    s1.print(); // li 10

    cout<<"**********"<<endl;
    Student s2={"zhang",20}; // 多参数构造函数
    s2.print(); // zhang 20

    return 0;
}

explicit 关键字 / 单参数隐式转换

对于单参数构造函数可以使用下面的方法进行实例化
如果构造函数只有一个参数,可将对象初始化为一个与参数的类型相同的值,且自动调用构造函数
这相当于一种隐式类型转换: 编译器会先拿Base构造一个临时对象temp,然后将10作为参数传给这个临时对象temp,在拿b(temp)进行拷贝构造,也就相当于发生隐式类型转换,即先构造在进行拷贝构造
注意:单参数赋值必须有对应的构造函数className(int val) 不能是默认构造函数className()(只定义默认构造函数,不能进行赋值运算)

注意,只有单参数的可以直接使用clsType=val,对于多参数的类,只能使用clsType={val1,val2}

总结:
对于单成员变量类,可以使用obj={val}obj=val两种方式,但是不能只定义默认构造函数(也就是默认的赋值运算符,只能生成的className(type val))
对于多成员变量类,只能使用obj={val1,val2}

#include <iostream>
using namespace std;

class Base
{
public:
	Base(int val):val(val){};
	void print(){cout<<this->val;}; // 自动成为内联函数
private:
	int val;
};

int main()
{
	Base b=10;
	b.print(); // 10 
	return 0;
}

explicit

只能用于构造函数
对于不想让单参数的构造函数发生隐式转换,可以对构造函数加上explicit关键字进行修饰,表明该构造函数是显示的,不能进行隐式转换

explicit Base(int val):val(val){};

例子

下面的例子,不能运行,报错

#include <iostream>
#include <string>

using namespace std;

class Student
{
private:
    string name;
    int age;
public:
    explicit Student(string name);
    explicit Student(string name,int age);
    void print();
};

Student::Student(string name)
    :name(name),age(10)
{
    // this->age=10;
    cout<<"单参数构造函数"<<endl;
}

Student::Student(string name,int age)
    :name(name),age(age)
{
    // this->age=age;
    // this->name=name;
    cout<<"多参数构造函数"<<endl;
}


void Student::print()
{
    cout<<this->name<<" "<<this->age<<endl;
}

int main()
{
    Student s1={"li"}; // 单参数构造函数
    s1.print(); // li 10

    cout<<"**********"<<endl;
    Student s2={"zhang",20}; // 多参数构造函数
    s2.print(); // zhang 20

    return 0;
}

析构函数

销毁对象时系统会自动调用一个函数来进行清理
特殊的成员函数,没有返回值,不需要显示调用
析构函数与类名重名 ~class_name

析构函数没有参数,不能被重载
一个类只能有一个析构函数,如果用户没有定义,编译器会自动生成一个默认析构函数

自定义的析构函数用来释放已经分配的内存

只有调用delete的时候才会执行析构函数
在栈上的会自动调用析构函数
在堆上的必须使用delete再回执行析构函数,即使程序运行完毕,也不会调用用户自定义的析构函数

class MyClass
{
private:
    const int m_len; // 数组长度
    int *m_arr; // 数组指针
    int *m_p; // 指向数组的第i个元素的指针

    int *getPtrIdx(int i); // 获取第i个元素的指针
public:
    MyClass(int len);

    void input(); // 从控制台输入数组元素

    ~MyClass();
};

MyClass::MyClass(int len)
    : m_len(len) //
{
    if (len>0)
    {
        m_arr=new int[len];
    }
    else
    {
        m_arr=NULL;
    }
}

// 这个函数很重要!!!
// m_p=getPtrIdx(i) 相当于判断是不是NULL(表达式的值是返回值)
// *(p+i)
void MyClass::input()
{
    for (int i=0;m_p=getPtrIdx(i);i++)
    {
        cin>>*(getPtrIdx(i));
    }
}

int * MyClass::getPtrIdx(int i)
{
    if (!m_arr || i<0 || i>m_len)
    {
        return NULL;
    }
    else
    {
        return m_arr+i; // 指针+idx,跨过idx个dataType类型
    }
}

MyClass::~MyClass()
{
    delete[] m_arr; // 释放数组
}

int main()
{
    MyClass *ptr=new MyClass;
    delete ptr;
}

类的嵌套 - 类的成员对象

一个类A的对象是 类B的成员变量,则称为类的的成员对象

补充初始化列表
必须使用列表初始化的方式对 成员对象 进行初始化(调动其构造函数),如果初始化列表当中没有对其进行初始化,且该类又没有默认构造函数(无参构造函数或全缺省构造函数)则不能实现该成员对象的初始化
先执行类的成员对象的构造函数,再调用自身的构造函数
对于析构函数,先调用类的析构函数,再调用类的成员对象的析构函数

class C1
{
public:
    C1(int x,int y);
    void print() const;
private:
    int m_x;
    int m_y;
};

C1::C1(int x,int y):m_x(x),m_y(y)
{

}
void C1::print()const
{
    cout<<"x:"<<this->m_x<<endl;
    cout<<"y:"<<this->m_y<<endl;
}

class C2
{
private:
    float m_z;
public:
    C2(float z=2);
    void print() const;
};

C2::C2(float z):m_z(z)
{
}

void C2::print()const
{
    cout<<"z:"<<this->m_z<<endl;
}


class MyClass
{
private:
    int m_p;
    C1 m_itemC1;
    C2 m_itemC2;
public:
    MyClass(int p,int x,int y);
    void print() const;
};

MyClass::MyClass(int p,int x,int y):m_p(p),m_itemC1(x,y)
{
    // m_p=p;
    // 当没有初始化列表的时候,这种方法时错误的
    // m_itemC1=C1(x,y);
}

void MyClass::print()const
{
    cout<<"p:"<<this->m_p<<endl;
    this->m_itemC1.print();
    this->m_itemC2.print();
}

int main()
{
    MyClass m_class=MyClass(3,4,5);
    m_class.print();

    return 0;
}

静态成员

  • 静态成员变量
  • 静态成员函数

不同的实例对象 占用不同的内存

静态成员变量

(相当于类属性)只分配一份内存,且在全局数据区
静态成员变量的内存即不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配(编译的时候就已经分配了),也就是没有在类外初始化的静态成员变量不能使用
静态成员变量不占实例对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问

静态成员变量必须在类声明的外部进行初始化(因为在全局数据区,当不赋值的时候,默认初始化为0);
public、private、protected修饰的静态变量都要在类外进行初始化
初始化的时候不能在使用static关键字
注意: 静态成员变量,在头文件中(类声明文件中)进行声明,在方法文件中(对应的cpp文件中)进行初始化(这是因为类声明位于头文件中,程序可能将头文件包含在其他多个文件中,如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误)

// xxx.h
class cls
{
	static int a;
};

// xxx.cpp
int cls::a=20;

在实例对象之间共享数据
静态成员变量即可以通过实例对象访问,也可以通过类名访问,但是要遵循public,private,protected的访问机制
对于public的静态成员变量,可以通过类名、实例对象名进行访问;对于private的静态成员变量,不可通过类名、实例对象名进行访问
实例对象可以访问静态成员函数、静态成员变量

class Student
{
private:
    static int num;
    char *name;
    int age;

    
public:
    Student(char *name,int age);

    void getNum();

    ~Student();
};

Student::Student(char *name,int age)
{
    this->name=name;
    this->age=age;
    this->num++;
}

void Student::getNum()
{
    cout<<Student::num<<endl;
}

Student::~Student()
{
}

int Student::num=0;

int main()
{
    Student stu1=Student("x",1);
    stu1.getNum(); // 1
    Student stu2("y",2);
    stu2.getNum(); // 2
    Student *stu3=new Student("z",3);
    stu3->getNum(); // 3
    // 匿名对象
    (new Student("k",4))->getNum(); // 4
    return 0;
}
// ERROR
//private:
//	int len=10;
//	char arr[len]; // 这里len还没有分配内存空间,只是一个符号

// 下面是正确的
private:
	static int len=10;
	char arr[len];

例子2

#ifndef __STACK_H__
#define __STACK_H__

typedef unsigned long Item;
class Stack
{
private:
    static const int MAX=12; // 这样写是正确的
    Item items[MAX];
    int top;
public:
    Stack(/* args */);
    ~Stack();
};

// const int Stack::MAX=10; 
/*
    // 下面是错误的

类内
private:
    static const int MAX;
    Item items[MAX];
类外
    const int Stack::MAX=10;    
*/

Stack::Stack(/* args */)
{
}

Stack::~Stack()
{
}
#endif

静态成员函数

静态成员函数只能访问静态成员变量、静态成员函数
静态成员函数可以通过类名来直接调用,但是编译器不会为它增加形参this,不需要当前对象的地址(所以不管有没有创建对象,都可以调用静态成员函数),(相当于类方法)
(编译器在编译一个普通成员函数时,会隐式增加一个形参this,并把当前对象的地址赋值给this,所以普通函数只能通在创建实例对象后通过对象来调用,即需要当前对象的地址)
静态成员函数再定义时也不能再使用static关键字

注意:this指针不能在static静态成员函数中使用
相当于静态方法
实例对象可以访问静态成员函数、静态成员变量(也就是可以用this调用静态成员函数和静态成员变量,但是不能在静态成员函数中使用this)

class Student
{
private:
    static int num;
    char *name;
    int age;

    
public:
    Student(char *name,int age);

    void getNum();
    static int static_getNum();

    ~Student();
};

Student::Student(char *name,int age)
{
    this->name=name;
    this->age=age;
    this->num++;
}

void Student::getNum()
{
    cout<<Student::num<<" "<<this->num<<endl;
}

int Student::static_getNum()
{
    cout<<"static "<<Student::num<<endl;;
    return num;
}


Student::~Student()
{
}

int Student::num=0;

int main()
{
    Student stu1=Student("x",1);
    stu1.getNum();
    Student stu2("y",2);
    stu2.getNum();
    Student *stu3=new Student("z",3);
    stu3->getNum();
    // 匿名对象
    (new Student("k",4))->getNum();

    int static_func_var1=stu1.static_getNum();
    int static_func_var2=Student::static_getNum();
    cout<<"static_func_var1:"<<static_func_var1<<"\t"<<"static_func_var2:"<<static_func_var2<<endl;

    return 0;
}

const 成员

  • const 成员变量 :见上面,const成员变量的初始化
  • const 成员函数
  • const 成员对象
// 下面是错误的
class cls
{
	private:
		const int num=0; // 这样写是不行的,声明的时候没有创建一个num对象
		char arr[num]; // 这样写是不行的 
};

// 下面是正确的
class cls
{
	private:
		static const int num=0; // 这样写是不行的,声明的时候没有创建一个num对象
		char arr[num]; // 这样写是不行的 
};

// 下面是正确的
class cls
{
	private:
		enum {num=0}; // 这样写是不行的,声明的时候没有创建一个num对象
		char arr[num]; // 这样写是不行的 
};

const 成员函数

const成员函数可以使用类中的所有成员变量,但是不能修改他们的值(不管成员是否具有const性质,都是不可修改的)
const成员函数要在声明和定义中都要加上const
const成员函数的调用,要根据public、private、protected的限制

本质上是 const classType* const this

public:
    void getItem() const;

// 类外
void ClassName::getItem() const
{
    /*code*/
}

例子

在下面的例子中,Base & operator-(const Base & b) const;该运算符重载会报错,因为const关键字修饰,使得不能被修改
但是对于 Base operator+(const Base & b) const; 本质上是利用了拷贝构造函数,重新修改了对象

/* .h */
#ifndef __TEST97_H__
#define __TEST97_H__

#include <iostream>
using namespace std;
class Base
{
private:
    int val;
public:
    Base(/* args */):val(10){};
    Base(int val):val(val){};
    ~Base();

    void getVal(){cout<<this->val;};

    Base operator+(const Base & b) const;
    Base & operator-(const Base & b) const;
    // friend Base operator*(const Base & b);
};

#endif
/* .cpp */

#include "test97.h"

Base Base::operator+(const Base & b) const
{
    Base temp;
    temp.val=this->val+b.val;

    return temp;
}

// 下面这个重载函数会报错
Base & Base::operator-(const Base & b) const
{
    this->val+=b.val;
    return *this;
}

// Base operator*(const Base & b)
// {

// }

Base::~Base()
{
}
/* main.cpp */
#include <iostream>
#include "test97.h"

using namespace std;

int main()
{
    Base b1;
    Base b2={10};

    b1=b1+b2;
    b1.getVal();
    
    return 0;
}

const 实例对象/成员对象

https://blog.csdn.net/Superman___007/article/details/116236597

const 实例对象只能调用该类(实例对象所属的类)的const成员(const成员变量、const成员函数)

// 下面两种方法等价
const class instance(params);
class const instance(params);

// 修饰const对象
const class *p=new class(params);
class const *p=new class(params);

下面会报错

#include <iostream>
using namespace std;
class Base
{
public:
	Base(a):a(a){};
	void print(){cout<<this->a;}; // void print() const  {cout<<this->a;} 修改成const成员函数就可以了
private:
	int a;
}

int main()
{
	const Base b=Base(10);
	b.print(); // ERROR print函数中并没有保护变量被修改的机制!!!
	return 0;
}

typedef 成员

typedef 定义的类型只能通过类名来访问

class Myclass
{
public:
    typedef int INT;
    static void staticFunc();
    void func();
};

void Myclass::staticFunc()
{
    cout<<"static func"<<endl;
}

void Myclass::func()
{
    cout<<"func"<<endl;
}

int main()
{
    Myclass mc;
    mc.staticFunc();
    mc.func();
    Myclass::staticFunc();
    Myclass::INT n=10;
    return 0;
}

类的作用域

每个类都会定义自己的作用域,在类的作用域之外,普通的成员只能通过实例对象来访问,静态成员可以通过对象或类名访问,typedef 定义的类型只能通过类名访问

定义在类外部的(该类)成员,必须指明所属的类作用域

class Myclass
{
public:
    typedef int INT;
    int func(int a);
private:
    int a;
};

Myclass:: INT Myclass::func(int a)
{
    this->a=a;
    return this->a;
}

int main()
{
    Myclass mc;
    mc.func(10);
    return 0;
}

另外一个例子 - 使用全局变量

int num=10;

class Myclass
{
private:
    int n;
    int &r;
public:
    Myclass();
};

Myclass::Myclass()
    :n(0),r(num)
{
}

int main()
{
    return 0;
}

类域进阶/this私有成员 – 还没看 很重要

https://blog.csdn.net/catwan/article/details/85335917
https://blog.csdn.net/weixin_48524215/article/details/115641607
https://flushhip.blog.csdn.net/article/details/80310327
https://www.cnblogs.com/fiteg/archive/2012/01/31/2332632.html
https://blog.csdn.net/qq_28110727/article/details/77071066

友元

其他类中的成员函数以及全局范围内的函数访问当前类的private成员

一个函数可以被多个类声明为

友元函数

友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数
友元函数可以访问当前类中的所有成员,包括public,protected,private
友元函数可以访问类的私有成员,和私有成员函数

值得注意的是,友元函数 func() 不能使用const进行修饰

例子1 类的成员函数作为友元函数

值得注意的是:定义在类中的友元函数,并不是成员函数
但是虽然友元函数不是成员函数,但是与其他成员函数的访问权限相同,遵循public, protected,private原则
在定义友元函数的时候,不能加类作用域运算符,且只在声明中使用friend关键字,不能在定义中使用friend关键字

/*.h*/
#ifndef __TEST97_H__
#define __TEST97_H__

#include <iostream>
using namespace std;
class Base
{
private:
    int val;
public:
    Base(/* args */):val(10){};
    Base(int val):val(val){};
    ~Base();

    void getVal(){cout<<this->val<<endl;};

    Base operator+(const Base & b) const;
    // Base & operator-(const Base & b) const; // 会报错 const不能修改
    friend Base operator*(const Base &a,const Base & b);
};

#endif
#include "test97.h"

Base Base::operator+(const Base & b) const
{
    Base temp;
    temp.val=this->val+b.val;

    return temp;
}

// Base & Base::operator-(const Base & b) const 错误的
// {
//     this->val+=b.val;
//     return *this;
// }

// 友元函数 可以访问私有变量
Base operator*(const Base &a,const Base & b)
{
    Base temp;
    temp=a.val*b.val;
    return temp;
}

Base::~Base()
{
}
/*main.cpp*/
#include <iostream>
#include "test97.h"

using namespace std;

int main()
{
    Base b1;
    Base b2={10};

    b1=b1+b2; 
    b1.getVal();
    
    Base b3;
    b3=b1*b2;
    b3.getVal();
    
    return 0;
}

例子2 - 非成员函数作为友元函数

class Student
{
private:
    char *name;
    int age;
public:
    Student(char *name,int age);
    
    friend void show(Student *pstu);

    ~Student();
};

Student::Student(char *name,int age)
{
    this->name=name;
    this->age=age;
    
}

Student::~Student()
{

}

void show(Student *pstu)
{
    // 通过友元函数访问私有成员
    // 友元函数不能直接访问类的成员,必须借助实例对象
    cout<<pstu->name<<",age: "<<pstu->age<<endl;
}

int main()
{
    Student stu1("li",18);
    show(&stu1);

    Student *pstu=new Student("zhang",19);
    show(pstu);

    return 0;
}

例子3 - 其他类的成员函数作为友元函数

类的提前声明
类的提前声明的使用范围是有限的,只有在正式声明(class Myclass{/*code*/};)一个类之后才能用它去创建对象
因为创建对象时位对象分配内存,在正式声明类之前,编译器无法确定应该为对象分配多大的内存,编译器只有在见到类的正式声明后(其实是见到成员变量),才能确定应该为对象预留多大的内存
在对一个类做了提前声明后,可以用该类的名字去定义指向该类型对象的指针变量,因为指针变量或引用变量本身的大小是固定的,与它所指向的数据的大小无关

// 必须提前声明,因为Student中使用了Myclass
class Myclass; // 提前声明一个类

class Student
{
private:
    char *name;
    int age;
public:
    Student(char *name,int age);
    
    void show(Myclass *mc);

    ~Student();
};

Student::Student(char *name,int age)
{
    this->name=name;
    this->age=age;
    
}


// 此时在Myclass 之前定义友元函数是错误,的因为没有找到该类的私有成员
// void Student::show(Myclass *mc)
// {
//     // 将该函数定义为Myclass类的友元函数
//     // 所以可以访问Myclass类中的私有成员
//     cout<<this->name<<"'s grade:"<<mc->grade<<endl;
// }


Student::~Student()
{

}

class Myclass
{
private:
    char *grade;
public:
    Myclass(char *grade);

    friend void Student::show(Myclass * mc);

    ~Myclass();
};

Myclass::Myclass(char *grade)
{
    this->grade=grade;
}



Myclass::~Myclass()
{
}

// 在这写才是对的
void Student::show(Myclass *mc)
{
    // 将该函数定义为Myclass类的友元函数
    // 所以可以访问Myclass类中的私有成员
    cout<<this->name<<"'s grade:"<<mc->grade<<endl;
}

int main()
{
    Student stu1("li",18);
    Myclass mc1("2");
    stu1.show(&mc1);

    Student *pstu=new Student("zhang",19);
    Myclass *mc2=new Myclass("3");
    pstu->show(mc2);

    return 0;
}

例子4 - << 运算符重载

只能使用友元函数的方式进行重载

void operator<<(ostream &os , clsType &obj)
{
	os<<obj.val<<endl;
}

连续输出

ostream & operator<<(ostream &os , clsType &obj)
{
	os<<obj.val;
	return os;
}

友元类 – 还没看

一般友元类不常用

拷贝构造函数

对象创建的两个阶段:分配内存空间,再进行初始化
初始化才是调用构造函数

拷贝构造,就是在初始化化阶段进行的,用其他对象的数据来初始化新对象的内存

当以拷贝的方式初始化一个对象时,会调用一个特殊的构造函数,就是拷贝构造函数

注意:下面两个可以构成重载,不会发生问题
cls(cls &cls_instance)
cls(const &cls_instance)

当用户没有显示的定义拷贝构造函数的时候,会自动生成一个默认的拷贝构造函数:
默认拷贝构造函数:使用老对象的成员变量对新对象的成员变量赋值(默认拷贝构造函数,一般应对简单的初始化操作)
cls(const cls &cls_instance) 默认生成
默认拷贝构造函数一般是够用的,没有必要显示的定义一个功能类似的拷贝构造函数,但是当类持有其他资源时,如动态分配的内存、打开的文件,指向其他数据的指针、网络连接等,默认拷贝构造函数就不能拷贝这些资源,必须显示的定义拷贝构造函数

例子1

str2、str3、str4都是拷贝构造函数

#include <iostream>
#include <string>

using namespace std;

void func(string str)
{
    cout<<str<<endl;
}

int main()
{
    string str1="zhangsan";
    string str2(str1);

    string str3=str1;
    string str4=str1+" "+str2;

    func(str1);
    func(str2);
    func(str3);
    func(str4);
    
    return 0;
}

例子2

值得注意的是,赋值运算符的重载,和调用拷贝构造函数 的区别

#include <iostream>
#include <string>

using namespace std;

class Student
{
private:
    string name;
    int age;
public:
    Student(string name,int age);
    Student(const Student &stu); // 拷贝构造函数声明

    void print();
};

Student::Student(string name,int age)
{
    this->age=age;
    this->name=name;
}

Student::Student(const Student &stu)
{
    this->age=stu.age;
    this->name=stu.name;

    cout<<"拷贝拷贝构造"<<endl;
}

void Student::print()
{
    cout<<this->name<<" "<<this->age<<endl;
}

int main()
{
    Student stu1("zhangsan",10);

    Student stu2=stu1; // 调用的是拷贝构造函数,即使没有进行赋值运算符的重载,也可以调用拷贝构造函数
    
    Student stu3(stu1);

    stu1.print();
    stu2.print();
    stu3.print();

    return 0;
}

例子3

Base::Base(const Base &instance)
{
    /*
        这里虽然是私有成员,但是拷贝构造函数传入来一个实例对象的引用相当于*this
        那么就相当于在类内访问私有成员,知道作用域在哪
    */ 
    this->a=instance.a;
    this->b=instance.b;
    this->count++;
    this->time=std::time((time_t *)NULL);
}

拷贝与赋值

注意:拷贝构造函数和赋值的区别
本质上是 初始化与赋值的区别
初始化:定义的同时赋值
赋值:只是赋值

即使没有显示的重载=赋值运算符,编译器也会以默认的方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量赋值给新对象,这和默认拷贝构造函数的功能类似
和默认拷贝构造函数类似,对于简单的类,默认的赋值运算符足够了,但是对于动态分配的内存、打开的文件,指向其他数据的指针、网络连接等,默认赋值运算符就不行了
例子

#include <iostream>
#include <string>

using namespace std;

class Student
{
private:
    string name;
    int age;
public:
    Student(string name,int age);
    Student(const Student &stu); // 拷贝构造函数声明
    
    Student & operator=(const Student &stu);

    void print();
};

Student::Student(string name,int age)
{
    this->age=age;
    this->name=name;
    cout<<"构造函数"<<endl;
}

Student::Student(const Student &stu)
{
    this->age=stu.age;
    this->name=stu.name;

    cout<<"拷贝构造"<<endl;
}

Student & Student::operator=(const Student &stu)
{
    this->name=stu.name;
    this->age=stu.age;

    cout<<"operator 重载"<<endl;

    return *this; // !!!
}


void Student::print()
{
    cout<<this->name<<" "<<this->age<<endl;
}

int main()
{
    Student stu1("zhangsan",10); // 构造函数

    cout<<"*****"<<endl;
    Student stu2=stu1; // 调用的是拷贝构造函数 // 拷贝构造

    cout<<"*****"<<endl;
    Student stu3(stu1); // 拷贝构造

    cout<<"*****"<<endl;
    stu1.print(); // zhangsan 10
    stu2.print(); // zhangsan 10
    stu3.print(); // zhangsan 10

    cout<<"*****"<<endl;
    Student s1("lisi",20); // 构造函数

    cout<<"*****"<<endl;
    Student s2("wangwu",30); // 构造函数

    cout<<"*****"<<endl;
    Student s3=s1; // 拷贝构造
    s3=s2; // operator 重载

    cout<<"*****"<<endl;
    Student s4=Student("zhao",30); // 构造函数+ 拷贝构造

    return 0;
}

拷贝构造与函数

函数参数传递

函数内的局部参数、形参,只有在栈上分配内存,也就是在函数声明和定义的时候参数没有被创建

下面在参数传递的时候也会调用拷贝构造函数

void func(clsType cls_instance){}

clsType cls_instance;
func(cls_instance) 

函数返回值

对于返回值是结构体和类对象时(值传递),为了防止局部对象被销毁,也为了防止通过返回值修改原来的局部对象,编译器并不会返回这个对象,而是根据这个对象先创建一个临时对象(匿名对象)以拷贝的方式进行,然后再将这个临时对象返回

下面在函数返回的时候也会调用拷贝构造函数

clsType func()
{
    clsType cls_instance
    return cls_instance;
}

clsType cls_instance=func();

例子

g++ file.cpp -o file -fno-elide-constructors 取消编译优化

#include <iostream>
#include <vector>
#include <string>

using namespace std;

class Base
{
private:
    vector<string> names;
public:
    Base();
    Base(const Base &b);
    ~Base();
};

Base::Base()
    :names(vector<string>{"li"})
{
    cout<<"默认构造函数, size: "<<names.size()<<endl;
}
Base::Base(const Base &b)
{
    this->names=b.names;
    cout<<"拷贝构造函数, size: "<<names.size()<<endl;
}

Base::~Base()
{
    cout<<"析构函数"<<endl;
}

Base func()
{
    Base b;
    return b;
}

int main()
{
    Base obj=func();

    return 0;
}

/*
	默认构造函数, size: 1
	拷贝构造函数, size: 1 // 局部变量到临时变量
	析构函数
	拷贝构造函数, size: 1 // 临时变量到主函数变量
	析构函数
	析构函数
*/

移动构造函数

见 ‘c++ 进阶其他’

对象与数组

实例对象数组

class MyClass
{
public:
    MyClass();
    MyClass(int n);
    MyClass(int n,int m);
    ~MyClass();
};
MyClass::Myclass()
{
    cout<<"init func1"<<endl;
}
MyClass::Myclass(int n)
{
    cout<<"init func2"<<endl;
}
MyClass::MyClass(int n,int m)
{
    cout<<"init func3"<<endl;
}

int main()
{
    MyClass arr1[2]; // 数组中的两个实例对象都调用无参构造函数
    MyClass arr2[2]={1,2}; // 数组中的两个实例对象都调用有参构造函数 (列表初始化)
    MyClass arr3[2]={1}; // 数组中的两个实例分别调用有参构造函数和无参构造函数 (列表初始化)
    MyClass *ptr=new MyClass[2]; // 数组中的两个实例都调用无参构造函数

    MyClass arr4[2]={1,MyClass(2,3)};//分别调用(1参数构造函数)和(2参数构造函数)

    MyClass arr5[2]={MyClass(1,2),MyClass(3,4)};

    // 见C语言指针数组
    Myclass* ptr2[3]={new MyClass(1,2),new MyClass(3,4)} // 相当于指针数组,指针所指向的数组中的每个元素都是一个指针(匿名对象)


    delete[] ptr; // 这个很重要 只要出现new就要进行释放


    return 0;
}

对象与指针

class Name
{
public:
    void method();
};

void method()
{

}

// 实际上
instance.method() 实际上是 method(&instance)
method(class_name * const p) 也就是将instance的地址赋值给常量指针  class_name *p=&instance

this指针

https://www.bilibili.com/video/BV1C7411Y7Kb?spm_id_from=333.337.search-card.all.click&vd_source=7155082256127a432d5ed516a6423e20 – 重要参考

this 指针是一个const 指针,不能被修改
this指针,实际上存放的是对象的首地址,也就是指向当前对象
this指针只能在类的内部使用

this指针不能在static静态成员函数中使用
非静态函数中的this指针可以访问静态成员变量(相当于实例对象名访问静态成员变量)

利用this指针不用对变量名进行刻意的编写,也就是可处理 成员变量和形参变量重名的问题

class MyClass
{
private:
    int a;
    void setItem(int a);
};
void MyClass::setItem(int a)
{
    this->a=a; // 指向当前的对象
}
int main()
{
    // MyClass C;
    // C.setItem(10);

    MyClass *C=new MyClass();
    C->setItem(10);
    return 0;
}
#include <iostream>
using namespace std;

class Student{
    public:
        int getAge(){
            return age;
        }

        void setAge(int age){
            this->age=age; // this->age 表示的是当前类中的属性,(特别用于属性与形参名相同的时候)
            			   // this->age 有点像self.age的作用
         }
		
		// 第二种用法
        Student setAge(int age){
            this->age=age;
            return *this; // this指针,实际上存放的是对象的首地址,引入如果要返回一个类对象,所以要*this
        }

    
    private:
        int age;
}; // 注意分号

int main(void)
{
    Student s;
    s.setAge(3);
    cout<<s.getAge()<<endl;
    return 0;
}

this指针的机制

this实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给this,不过this这个形参是隐式的,并不出现在代码中,而是在编译阶段由编译器默默的将他添加到参数列表中
this作为隐式形参,本质上是成员函数的局部变量,所以只能在成员函数的内部,并且只有在通过对象调用成员函数时才给this赋值

#include <iostream>
using namespace std;

class Student{
    public:
        int getAge(){
            return age;
        }

        // void setAge(int age){
        //     this->age=age; // this->age 表示的是当前类中的属性,(特别用于属性与形参名相同的时候)
        // }

        Student setAge(int age){
            this->age=age;
            return *this;
        }

        void test(){
            cout<<"this 指针存放的是谁的地址"<<this<<endl; //0x61fe1c
        }
        
        /*
        实际上编译器是自动加上了一个该对象类型的 指针
        void test(Student* this){
            cout<<"this 指针存放的是谁的地址"<<this<<endl; //0x61fe1c
        }
        */
    
    private:
        int age;
}; // 注意分号

int main(void)
{
    Student s;
    // s.setAge(3);
    // cout<<s.getAge()<<endl;
    
    s.test();
    /*
    调用的时候相当于传入了这个对象的地址,void test(Student* this)
    s.test(&s) 
    */

    cout<<"s 实例的地址"<<&s<<endl; //0x61fe1c
    return 0;
}

静态成员函数不能访问this指针

// static void lazy(){
//     cout<<this->age<<endl; // 静态成员函数不能使用this函数
// }

class/struct

c语言中 struct 不能包含成员函数
c++中 struct 可以包含成员函数

class 类中成员默认是private属性;类的继承默认是private继承
struct 结构体中成员默认是public属性;struct继承默认是public继承

class 可以使用模板
struct 不能使用模板

建议在c++中,class定义类,struct中定义结构体,不要进行混用

拷贝

  • 深拷贝
  • 浅拷贝

默认拷贝构造函数一般是够用的,没有必要显示的定义一个功能类似的拷贝构造函数,但是当类持有其他资源时,如动态分配的内存、打开的文件,指向其他数据的指针、网络连接等,默认拷贝构造函数就不能拷贝这些资源,必须显示的定义拷贝构造函数

深浅拷贝细节

int a=10;
int b=a; // 浅拷贝,虽然是内存地址不同,指向的数据也不同(应该叫做不完全拷贝)

classType class_instanceA;
classType class_instanceB=class_instanceA; // 默认拷贝构造函数,属于浅拷贝,实际上是不完全拷贝

例子

下面的例子中 如何直接使用默认拷贝构造函数,则是将arr1.p 直接赋值给arr2.p 导致arr2.p和arr1.p指向了同一块内存,所以会互相影响

#include <iostream>
using namespace std;

class Array
{
private:
    int len;
    int *p;
public:
    Array(int len);
    Array(const Array &arr);
    ~Array();

    int operator[](int i) const;
    int &operator[](int i);

    int length() const;

};

Array::Array(int len)
{
    this->len=len;
    this->p=(int *)calloc(this->len,sizeof(int));
}

Array::Array(const Array &arr)
{
    this->len=arr.len;
    this->p=(int *)calloc(this->len,sizeof(int));
    memcpy(this->p,arr.p,this->len*sizeof(int));

    cout<<"拷贝构造"<<endl;
}

Array::~Array()
{
    free(this->p);
}

int Array::operator[](int i) const
{
    cout<<"重载1"<<endl;
    return this->p[i];
}

int & Array::operator[](int i)
{
    cout<<"重载2"<<endl;
    return this->p[i];
}

int Array::length() const
{
    return this->len;
}

void printArray(const Array &arr)
{
    int len=arr.length();

    if (len==0)
    {
        cout<<"empty"<<endl;
        return ;
    }

    for (int i=0;i<len;i++)
    {
        if (i==len-1)
        {
            cout<<arr[i]<<endl;
        }
        else
        {
            cout<<arr[i]<<", ";
        }
    }
} 

int main()
{
    Array arr1(10);

    for (int i=0;i<10;i++)
    {
        arr1[i]=i;
    }

    cout<<"********"<<endl;
    Array arr2=arr1; // 注意这里调用的是拷贝构造函数 不是重载赋值运算符
    arr2[5]=100;
    
    cout<<"********"<<endl;
    printArray(arr1);
    printArray(arr2);

    return 0;
}

深拷贝与浅拷贝

如果一个类拥有指针类型的成员变量,绝大部分情况下需要深拷贝,因为只有深拷贝才能将指向指针的内容再赋值一份出来,让原有对象和新生对象相互独立,不影响
通常情况下,如果类成员变量没有指针,浅拷贝就可以

另外需要深拷贝的情况就是在创建对象时进行一些预处理操作

例子2

#include <iostream>
#include <ctime>
#include <windows.h>
using namespace std;

class Base
{
private:
    int a;
    int b;
    time_t time; // 对象创建时间
    /*
        静态成员变量,相当于类属性
        私有成员,遵循私有成员访问权限
        this 和类名都可以进行方法
        要在类外进行初始化
    */
    static int count; // 创建过的对象的数目,静态成员变量
public:
    Base(int a=0,int b=0);
    Base(const Base &instance);
    
    int getCount() const;
    time_t getTime() const;
};

int Base::count=0;

Base::Base(int a,int b)
{
    this->a=a;
    this->b=b;

    this->count++;
    this->time=std::time((time_t *)NULL);
}

Base::Base(const Base &instance)
{
    /*
        这里虽然是私有成员,但是拷贝构造函数传入来一个实例对象的引用相当于*this
        那么就相当于在类内访问私有成员,知道作用域在哪
    */ 
    this->a=instance.a;
    this->b=instance.b;
    this->count++;
    this->time=std::time((time_t *)NULL);
}


int Base::getCount() const
{
    return this->count;
}
time_t Base::getTime() const
{
    return this->time;
}

int main()
{
    Base instance(1,2);
    cout<<instance.getCount()<<" "<<instance.getTime()<<endl;

    Sleep(3000);

    cout<<"********"<<endl;

    Base instance2=instance; // 注意这调用的是拷贝构造函数,而不是重载赋值运算符
    cout<<instance2.getCount()<<" "<<instance2.getTime()<<endl;

    return 0;
}

赋值

注意:拷贝构造函数和赋值的区别
本质上是 初始化与赋值的区别
初始化:定义的同时赋值
赋值:只是赋值

即使没有显示的重载=赋值运算符,编译器也会以默认的方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量赋值给新对象,这和默认拷贝构造函数的功能类似
和默认拷贝构造函数类似,对于简单的类,默认的赋值运算符足够了,但是对于动态分配的内存、打开的文件,指向其他数据的指针、网络连接等,默认赋值运算符就不行了

例子

#include <iostream>
using namespace std;

class Array
{
private:
    int len;
    int *p;
public:
    Array(int len);
    Array(const Array &arr);
    ~Array();

    int operator[](int i) const;
    int &operator[](int i);

    Array & operator=(const Array &arr); // 赋值运算符重载

    int length() const;

};

Array::Array(int len)
{
    this->len=len;
    this->p=(int *)calloc(this->len,sizeof(int));
}

Array::Array(const Array &arr)
{
    this->len=arr.len;
    this->p=(int *)calloc(this->len,sizeof(int));
    memcpy(this->p,arr.p,this->len*sizeof(int));

    cout<<"拷贝构造"<<endl;
}

Array::~Array()
{
    free(this->p);
}

int Array::operator[](int i) const
{
    // cout<<"重载1"<<endl;
    return this->p[i];
}

int & Array::operator[](int i)
{
    // cout<<"重载2"<<endl;
    return this->p[i];
}

int Array::length() const
{
    return this->len;
}

Array & Array::operator=(const Array &arr)
{
    if (this != &arr) // 判断是否是给自己赋值
    {
        this->len=arr.len;
        free(this->p); // 释放原来的内存
        this->p=(int *)calloc(this->len,sizeof(int));
        memcpy(this->p,arr.p,this->len*sizeof(int));
    }

    cout<<"=重载"<<endl;
    return *this;
}

void printArray(const Array &arr)
{
    int len=arr.length();

    if (len==0)
    {
        cout<<"empty"<<endl;
        return ;
    }

    for (int i=0;i<len;i++)
    {
        if (i==len-1)
        {
            cout<<arr[i]<<endl;
        }
        else
        {
            cout<<arr[i]<<", ";
        }
    }
} 

int main()
{
    Array arr1(10);
    for (int i=0;i<10;i++)
    {
        arr1[i]=i;
    }
    printArray(arr1);

    cout<<"********"<<endl;
    Array arr2(5);
    for (int i=0;i<5;i++)
    {
        arr2[i]=i;
    }

    printArray(arr2);

    cout<<"********"<<endl;
    arr2=arr1;


    return 0;
}

补充

类与枚举类型

在类中定义的枚举类型,作用域是整个类,因为枚举类型相当于符号常量
也就是只是创建了符号常量,并不是创建枚举类型的变量

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值