2.类

  2021-8-14 


ch.7 class

  1. C++中两种显式对象创建方式

    ①静态内存中:Object obj(param)使用若无构造参数则不能写括号如:Object obj,否则会被当成函数声明)

    Object obj;执行默认初始化,也可以使用Object obj = Object()执行值初始化

    对于默认初始化和值初始化,见后面

    ②动态内存中:Object *p = new Object(param)(这个p是个指向对象的针,访问对象成员需要用(*p).或者->

    与C不同,c中struct方式必须在类名前加struct,且c struct不能有默认参数;c++中类名前可以加class/struct关键字,也可以不加

    与JAVA中的new写法完全不同

    第一种属于自动变量写法,在c/c++中都是会自动分配内存(新建对象 /.cpp)的;第二种属于new关键字,动态分配内存

    这两种方式主要的不同在于对象的存储时间。当执行①时,它作为自动变量被创建,这意味着当对象出了作用域时也会自动销毁。而使用new关键字,②这种方式时,对象所拥有的内存是动态分配的,这表示直到你调用delete()方法对象才会被销毁,否则一直存在。当需要用动态分配内存来处理时,你应该只使用动态分配的方式,也就是说,当你可以使用动态分配内存的时候就不要使用自动变量。

    //方法1
    #include <iostream>
    using namespace std;
    struct test
    {
        int a = 10;
        test(int f)
        {
            a = f;
        }
        test get_obj()
        {
            return *this;
        }
    };
    int main()
    {
        test t(-10);
        test temp = t.get_obj();
        cout<<t.a<<endl;
    }
    
    //方法2
    #include <iostream>
    using namespace std;
    struct test
    {
        int a = 10;
        test(int f)
        {
            a = f;
        }
        test& get_obj()
        {
            return *this;
        }
    };
    int main()
    {
        test *t = new test(-10);
        test &t_ref = t->get_obj();
        cout<<t->a<<endl;
    }
    
    //output:-10
  2. c++中常用非成员接口函数来操作类,比如JAVA中的toString在C++中可以定义一个非成员接口函数print,传入参数为目标对象的引用,来实现(下面为该函数的定义声明)

    void print(const Object&);

  3. c++中的this是一个指向对象的常量指针,隐式初始化。调用成员函数的时候才初始化this。

    this默认情况下是指向非常量的常量指针,这样就会在指向常量对象成员的时候出问题,所以c++语言的做法是允许把const关键字放在成员函数的参数列表之后,这样就会在调用常量成员函数的时候初始化一个指向常量的常量指针

    int func() const{};

  4. 类可以先只在内部声明成员函数,然后再在外面实现之

    要使用作用域运算符::

    #include <iostream>
    using namespace std;
    class Object
    {
    public:
        int a = 10;
        void func();
    //即使是私有函数,也可以在类外面实现
    private:
        void test();
    };
    void Object::func()
    {
        cout<<"func";
    }
    void Object::test()
    {
        cout<<"test";
    }
    
    int main()
    {
        Object obj;
        obj.func();
    }
    
  5. 函数可以返回this对象

    //在外部定义一个Object类内的函数get_obj()
    Object& Object::get_obj()
    {
    	return *this
    }
    
    Object obj;
    Objcet &ref = obj.get_obj(); //ref是obj的引用

    this指针指向对象,所以*this解引用后返回对象本身

    这里返回的是Object&即原对象的引用

    这样就可以实现建造者模式:

    //每一次函数调用都返回了&obj,即原对象的引用,所以可以继续调用来修改原对象
    obj.set_hight(10).set_width(20).set_color(red);

    如果不返回引用,即下面这一段,这时候返回的就是一个原对象的拷贝!(但如果函数返回作为左值则必须返回引用类型)

    Object Object::get_obj()
    {
    	return *this
    }
    
    Object obj;
    Objcet obj_copy = obj.get_obj(); //obj_copy是obj的拷贝,改变obj_copy不会改变obj

    如果不想其他人修改返回的对象,可以返回一个指向常量对象的引用(常量引用),即

    const Object& Object::get_obj()
    {
    	return *this
    }

    返回常量引用之后,就不能再进行任何操作了,因为只能用常量函数操作常量对象,非常量函数不能操作非常量对象,这里原对象并没有被转换成常量对象,而是引用是常量引用,常量引用仅对引用可参与的操作做出了限定,对于引用的本身是不是一个常量未作限定(见const部分)

    #include <iostream>
    using namespace std;
    class Object
    {
    public:
        int a = 10;
        const Object& func();
        void set();
    };
    const Object& Object::func()
    {
        return *this;
    }
    void Object::set()
    {
        this->a = -10;
    }
    
    int main()
    {
        Object obj;
        const Object &ref = obj.func();
        ref.set(); //报错:ref是常量引用,该引用将目标对象当成常量,不能调用非常量函数set()
        obj.set(); //正确:obj是非常量对象本身,可以调用非常量函数set()
    }

    可以基于const重载,即同一个函数保留返回const Object&版本和Object&版本,常量对象会自动调用常量版本,非常量对象会自动调用非常量版本

  6. 与JAVA一样,编译器在发现类不包含任何构造函数的时候会自动生成一个默认构造器,一旦自己定义了一个构造器,就不会有默认构造器了

    在定义的时候,可以Object()=default;来要求编译器生成默认构造函数。

  7. 由于this是一个常量指针(指向对象),所以用的时候要以指针的用法使用

    因为以前常用JAVA,这里很容易搞忘

    struct test
    {
        int a = 10;
        test(int a)
        {
            this->a = a;  //或(*this).a;就算再在里,也别忘了先对this解引用才能使用,要用->指针成员访问符
        }
        test get_obj()
        {
            return *this; //repo
        }
    };
    
  8. Cpp中有两种创建类的方式,class和struct,前者默认private,后者默认public,其它用法完全一致。struct主要是为了兼容c。

    class和struct最后一个大括号后面都要加分号;

    不能在class、struct之前加访问修饰符,要想整个类都public,使用struc;要想整个类都private,使用class;要想控制成员访问权限,在类定义中使用public:private:protect:语句

    class定义中的默认构造器是public的,但非默认构造器是private的!

    所以一定要在class中把构造器添加到public:修饰符中

  9. 对于友元,三种使用形式

    ①声明非成员函数为友元(该函数可以访问本类公私有成员)

    friend <函数声明>

    ②声明某类为友元(某类中的所有成员可以访问本类公私有成员)

    friend struct/class <class_name>

    ③声明某类中的某成员函数为友元(某类中的某成员函数可以访问本类公私有成员)

    friend <带有作用域声明符::的函数声明>

    类和非成员函数的声明不是必须在它们的友元声明之前。(只要注意访问类的时候该类必须已经定义即可)

    class B
    {
        //这里A类还没声明,但是先在B类声明友元,这是可以的
        friend class A; 
    public:
        int a = 10;
    };
    class A
    {
        void func()
        {
            B b;
            int s = b.a;
        }
    };
    

    但对于第三种类中的成员函数一定要注意顺序:被声明友元的函数必须在友元声明之前声明,比如我们想要A类中的func()方法访问B类中的私有成员a

    class A
    {
    public:
        //1.由于B要声明A中的func为友元,所以A中的func()必须先定义
        //2.但是A中的func()要访问B,此时B还没声明,所以func()只能先声明,不实现内容
        void func(); 
    };
    class B
    {
        friend void A::func();
        int a = 10;
    };
    //在B实现完毕,也声明了A::func()是友元之后,再实现A::func()
    void A::func()
    {
        B b;
        cout<<b.a<<endl;;
    }

    友元不具传递性和继承性

  10. 编译器处理类定义分两阶段进行:

    ①首先,编译成员的所有声明(变量声明、函数声明等)

    ②直到类全部可见后才编译函数体

    这里指的是类中的声明函数体两者的处理顺序,成员函数内的声明就完全按顺序执行了。

  11. 初始化

    构造函数初始值表起一个初始化声明的作用(const、引用必须借用这个语句进行显式初始化)

    注意,构造函数初始值表是初始化声明,而不是赋值。

    注意:构造函数初始值表只是定义,而不是声明,表中的变量名需要先在类中声明。且初始化顺序与声明顺序而不是定义顺序一致。

    //定义并初始化
    string foo = "Hello World!";  //方法一(定义处声明初始化)
    <Constructor>(param):var1(_var1),var2(_var2) {} //方法二(构造函数初始值表)其中var1的值被初始化为_var1
    
    //默认初始化为空的String对象
    string bar;
    //为bar赋值
    bar = "Hello World!";

    初始化与赋值,两者发生在不同的时间。构造函数体一开始执行,初始化就完成了,然后才会执行函数体内的赋值操作。

    由于const和引用不能被赋值,所以他们只能直接初始化成目标值。所以const和引用要么直接作为形参被调用(隐式初始化),要么通过构造函数初始值表被显式初始化(即在类中声明引用/const变量名,然后在构造函数初始值表中初始化它)。

    注意,初始化列表中不需要用this指定是本类的成员!括号外(被初始化变量)默认就是本类的成员;调用父类构造器的时候也不需要用作用域运算符表明是父类,编译器老懂哥了。

    未提供默认构造函数的类类型也必须被显式初始化!(不然程序不知道怎么初始化它;可以使用两种初始化方法)

    对于基本类型也是,int a(10)直接初始化,int a = 10拷贝初始化

    建议直接初始化而非赋值,这样效率更高,也避免一些成员必须被初始化的情况。

    并非只能在构造函数中初始化,简单对象也可以直接在类中自己的声明处初始化

  12. 默认初始化与值初始化【】

    C++里的初始化五花八门,禁用非常用初始化!

    https://zhuanlan.zhihu.com/p/365769082

  13. 字符串字面值

    字符串字面值的类型实际上是由常量字符构成的数组

    字符数组的特点是末尾元素是空字符'\0',所以字符串字面值的实际长度要比它的内容多1

    字符串末尾是没有空字符的,字符串的长度就是其内容长度

    创造一个字符串,并将一个字符串字面值赋值给它,实际上发生了字符数组到字符串的隐式类型转换

    //发生了字符数组到字符串的隐式类型转换
    //字符串字面值"hello" 实际上是一个字符数组,实际上是'h','e','l','l','o','\0'
    //str是字符串,隐式转换之后成了"hello"
    string str = "hello"

    P.S.编译器只会执行一步隐式类型转换,所以当转换成字符串之后如果还要用编译器隐式类型转换成其它类型,就会报错

  14. 构造函数形参类型到该类类型的隐式转换

    struct Object
    {
        Object(string str){}
    };
    void print(Object obj){};
    int main()
    {
        string str = "abc";
        print(str); //隐式创建Object对象并调用构造器,参数为str
        Object obj = str; //隐式创建Object对象并调用构造器,参数为str
    }

    这种隐式类型转换很不安全,所以建议对所有一个参数的构造器声明explicit禁用隐式类型转换

    可以使用构造函数形参类型到该类类型的显式类型转换:

    print(static_cast<Object>(str))  //显式创建Object对象并调用构造器,参数为str
  15. c++的静态成员

    static声明静态成员(static成员可以类内部声明,外部定义,但是注意static关键字只能写在类内部;即使一个常量静态数据成员在类内部被初始化了,通常也应该在类的外部定义一下该成员)

    注意静态变量和全局变量都是只能在全局被声明的,全局指在一切函数和类声明之外(包括main函数之外!)

    通过作用域运算符使用静态成员:Object::<static_member>

    通过实例对象使用静态成员:obj.<static_number>

    通过指向实例对象的指针使用静态成员:obj_pointer-><static_number>

    类内无需作用域运算符

    静态数据成员可以是不完全类型(即静态数据成员的类型可以是它所属的类类型;指针成员也可以指向它所属的类的类型),非静态成员必须是完全类型。静态成员也可以作为默认实参。

  16. 注意:类内定义的static变量只是声明,不会初始化!需要在类外再次定义才会初始化!

    struct A
    {
        static int a;  //无法使用!static变量在类内只是声明!
    }
    int a; //这时候在外部进行了初始化(初始化的时候不能再写static)
  17. 类内初始化

    类内非构造函数外可以进行简单变量的初始化,且在定义语句中初始化的时候,括号初始化和赋值初始化是等价的,比如:

    //类内定义处初始化,以下皆可
    int num = 1;
    int num = {1};
    int num {1};    //2、3种是列表初始化,单独一个变量慎用
    int num = (1);
    int num (1);

    ATT!在struct的类内无法进行int a(1)初始化,因为会和函数int num()有歧义。
    所以在初始化的时候还是建议用=


且听风吟