7-类

来源:互联网 发布:同济启明星软件价格 编辑:程序博客网 时间:2024/05/19 20:18

  • 类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现分离的编程技术,通常来说,类的接口包括用户所能执行的操作,类的实现包括类的数据成员,
    负责接口实现的函数体,以及定义类所需的各种私有函数;
  • 封装实现了类的接口和实现的分离,在设计类时,首先应该定义抽象的数据类型,其次是决定成员函数的功能,成员函数的声明必须在类里面,定义和实现可以放在
    类的外部
  • 定义在类里面的函数都是隐式的inline函数;
  • 成员函数可以通过this指针来来访问对象里面的成员变量,在使用this指针进行成员变量的访问时,编译器负责将某个确定对象的地址初始化this指针,在
    成员函数的内部可以直接调用函数的对象的成员,因为this默认已经被初始化为这个对象;
  • 在成员函数的内部是不需要通过this指针来访问函数的对象的成员的;
  • this指针是一个常量指针,不允许进行改变;
  • this指针的类型是指向类类型的非常量版本的常量指针,假如this指针的类型是Sales_data *const那么this指针在进行初始化时,就会出错,因为this指针是一个常量,不允许进行改变;
  • C++通常允许将const放在成员函数的参数列表之后,用于表明this是一个指向常量的指针,,该成员函数就是常量成员函数;
  • 常量对象或者常量对象的引用或者指针都只能调用常量成员函数;
类作用域和成员函数
  • 类本身就是一个作用域,编译器首先编译成员变量的声明,其次处理成员函数体,所以及时成员变量在成员函数提之后声明,也仍然能够进行使用;
  • 成员函数的声明必须放在类作用域里面,但是成员函数的定义可以放在类作用里面,也可以放在成员函数的外面(需要使用类作用域运算符进行限定);
  • 当需要将左值对象进行返回时,函数的返回值类型应该定义为引用类型;
  • 非成员函数的定义:
    • 非成员函数不应该在类里面进行声明,但是建议和类的声明放在同一个文件里面;
    • 输入函数定义istream &read(istream &is,Sales_data &item){ is >> " " >> " " >> " " };
    • 输出函数定义ostream &print(ostream &os,Salesdata &item){os << " " << " " << };
  • 构造函数
    • 在类里面需要通过定义一个或者几个成员函数来控制其对象初始化过程的函数成为构造函数;
    • 只要类的对象被创建,就需要执行构造函数;
    • 构造函数的名字必须和类的名字相同;
    • 构造函数没有返回值;
    • 构造函数有一个可能为空的参数列表,以及一个可能为空的函数体;
    • 类可以包含多个构造函数,但是构造函数之间的参数类型和参数个数必须有所区别(也就是函数重载的条件);
  • 默认构造函数
    • 默认构造函数没有实参,默认构造函数是编译器自动创建的;
    • 如果存在类里面的初始值,就用它来初始化,否遭使用默认初始化函数;
    • 需要注意的几点:
      • 编译器只有在发现类里面没有任何构造函数的情况下,才会自动生成默认的构造函数;
      • 很多时候,默认的构造函数会执行错误的操作,比如:内置类型或者是复合类型(指针和数组)在进行初始化时,就会执行错误的操作;
      • 编译器无法为某些类类型的并且该类类型不包含默认的构造函数;
      • 以上情况都需要自己定义构造函数来控制对象的初始化;
    • Sales_data() = default表示的含义是:如果需要函数默认的行为,就需要在函数的初始化列表之后添加=default来要求编译器生成构造函数,也就是在定义
      了其他构造函数之后,编译器就不会自动生成默认的构造函数,使用这个选项可以使编译器生成默认的构造函数;
    • 如果= default的函数声明在类的里面,表示这个函数是内联的,如果声明在类的外面,默认就不是内联的;
    • 构造函数初始化列表:构造函数:{}之间的部分表示的就是构造函数的初始化列表,用于为新创建的数据成员进行赋值;
    • 如果编译器不支持类内部初始值,所有的构造函数都应该显示的初始化,每个内置类型的成员;
    • 如果需要在类的外面定义成员函数,需要使用类名::用来说明函数是一个累的成员函数;
    • 没有出现构造函数初始化列表的成员,将通过类内进行初始化,或者执行默认初始化;
  • 拷贝
    • 初始化变量,用值的方式传递或者返回一个对象
  • 赋值
    • 使用赋值运算符就会发生对象的赋值操作
  • 析构
    • 当对象不在存在时,就会执行销毁操作,局部对象在创建它的块结束时,就会销毁,这些操作编译器可以自动进行生成,但是建议在以下情况,需要自己定义这些操:
      • 当类需要分配类之外的资源时;
  • 访问控制和封装:
    • 访问控制符:
      • public:滞后的成员在整个程序里面都可以被访问到;
      • private:表示之后的成员可以被类的成员函数访问到,但是无法被使用该类的函数访问到;
        对于类classstruct的区别:
    • 根本区别是两者的默认的访问控制权限不同,struct的默认访问控制权限是public,class的默认访问控制权限是private;
  • 友元函数
    • 将类的非成员函数或者类定义为friend可以用于访问该类的非公有成员(private,protected);
    • 关于有元的声明只能在类里面,集中进行友元的生命;
    • 友元成员不受当前类的访问控制级别的限制;
    • 通常情况下,友元声明和函数声明并不一样,在进行友元声明之后,还需要进行函数声明;友元函数的声明并非一定要在类的外面;
  • 为什么要进行封装
    • 确保用户代码不会修改封装对象的状态;
    • 被封装的累的具体实现细节可以随时改变,无需调整用户级别的代码;
    • 盲目的使用友元函数会破坏类的封装特性;
  • 关于类需要注意:
    • 在类里面除了可以用于定义成员函数和成员变量之外,还可以用于定义某种类型在类里面的别名,通常使用public进行封装,这也是为了隐藏具体的事项细节
    • 使用typedef必须先定义类型名,然后才能使用,因此类型成员通常出现再类开始的地方;
    • 定义在类里面的成员函数是自动内联的,inline可以用于在类里面或者类外面进行修饰;
    • 类的成员函数在满足重载的条件时,也是可以进行重载的;
    • 可变数据成员
      • 如果类里面的 const类型函数的数据成员希望得到修改,需要将这个成员变量声明为mutable size_t access_ctr类型,那么access_ctr即使在一个
        const成员函数里面,这个变量的值也是能够得到修改的;
    • 类成员函数的默认初始值:std::vector<Screen> screen{Screen(24,80, ' ')};
      • 首先Screen()调用构造函数,并且将值传递给vector<Screen>创建出一个临时的vector对象;
      • 类里面的初始值只能有以下几种情况
        • 使用=进行初始化;
        • 使用{}的直接初始化形式;
    • 返回*this的成员函数
      • 返回引用的函数是左值,表示函数返回的是对象本身而不是对象的副本;
      • 一个const成员函数如果是按照引用的方式返回*this,那么返回值的类型将是一个常量引用,无法进行修改;
    • 是否是const成员函数是可以进行重载的条件之一;
    • 对于非常量版本的成员函数和常量版本的成员函数应该实现常量对象和非常量对象的最佳匹配,所以有必要对上述进行重载实现最佳匹配;
    • 具体调用那个版本的成员函数,取决于对象是常量还是非常量;
  • 类类型
    • 类本身就是一种类型,即使类本身的成员变量和成员函数完全一样,这两个类也是不同的类型,类的名字可以作为类类型的名字;
    • 类是可以先声明而不定义的,就像函数一样,class Screen,成为前向声明,在未进行定义之前,又称为不完全类型;
    • 不完全类型的使用场景
      • 可以用于定义指向这种类型的指针或者引用;
      • 也可以声明但是不能够定义以不完全类型作为参数或者返回类型的函数;
  • 友元函数补充:
    • 类可以把其他类定义为友元,也可以把其他类的成员函数定义为友元;
    • 友元函数可以定义在类的内部,这样是隐式内联;
    • 假设A类需要访问B类的内部数据时,需要在B类声明A类为友元类;
    • 友元关系不具有传递性,所以每个类需要控制好自己的友元类和友元函数;
    • 如果具体到A类的某个成员函数需要访问B类的成员变量时,那么就需要将A类的成员函数在B类里面设置为友元函数,同时需要加上friend A:: name()
    • 对于重载函数来说,每个重载函数如果需要设置为友元,那么每个函数都需要进行声明;
    • 友元声明和作用域:
      • 当友元函数在某个类作用域里面进行声明,隐式的假定该名字在该作用域里面是有效的;
      • 即使友元函数在类的内部已经定义,那么仍然需要在类的外部进行声明,使函数在类外部是可见的;
      • 友元函数可以在类内部定义和声明,但是必须在使用之前在类的外部进行声明或者定义,才可以在类的外部或者内部调用;
  • 类的作用域
    • 在类的作用域之外,普通数据和函数成员只能由对象,引用或者指针使用成员访问运算符来进行访问;
    • 对于类类型的成员可以通过作用域运算符来进行访问;
    • 在类的作用域的外部,成员函数的名字会被隐藏,所以在类的外部定义成员函数时,需要指定类名和函数名;
  • 名字查找和类的作用域
    • 类的定义分为两步进行处理:1.编译成员的声明;2.直到类全部可见后,编译函数体;
    • 按照上述行为,成员函数就可以使用成员变量名,否则在使用成员变量名时,就会出错;
    • 编译器在查找某个变量或则函数的声明时,只会向上进行查找;
    • 一般来说内层作用于可以重新定义外层作用域的名字,及时改名字已经在内层作用中使用过;
    • 如果是在类中如果类里面的成员使用了外层作用中的某个名字,同时该名字代表一种类型,那么在类里面是不可以重新定义该名字的;
    • 对于类里面类型名的定义行为,一般在类的开始进行类型的定义;
    • 成员定义中普通块作用域的名字查找
      • 在成员函数内查找该名字的声明只有在函数使用之前的声明才能够被考虑;
      • 如果在成员函数中没有找到,则在类里面继续进行查找,这时类的所有成员都会被考虑;
      • 如果再类里面没有找到该成员函数的声明,就会在成员函数定义之前的作用域里面进行查找;
      • 如果编译器在在函数和类的作用域都没有找到名字,就会在外围作用域进行查找,如果在成员函数需要使用全局定义的变量需要使用类作用限定符;
      • void Screen::dummy_fcn(pos height){ cursor = width * ::height }
      • 在文件中名字的出现处对其进行解析:
        • 如果成员定义在类的外部时,名字查找的第三步,不仅需要考虑类定义之前的全局作用中的声明,还需要考虑在成员函数定义之前的全局作用域中的声明
  • 构造函数补充:

    • 构造函数的初始值列表:
      • string str执行的是默认初始化,无参初始化;
      • 初始化和赋值是有区别的,赋值的前提是赋值运算符左边的已经存在;
      • 对于构造函数来说,也是有以上的区别的,如果使用了=执行的就是赋值操作,如果是在初始化列表中进行初始化,那么执行的就是定义并进行初始化,但是
        没有执行赋值操作;
      • 需要牢牢记住的一点就是如果需要执行赋值操作,那么一定是有=号的;
      • 初始化操作和赋值操作在成员是const或者是引用的情况下是有区别的,必须进行初始化操作,而不能够进行赋值操作;
      • 当成员属于某种类类型,并且这个类没有定义默认的构造函数时,这个成员必须进行初始化操作;
      • 例如:class ConstRet{ private:int i; const int ci; int &i; };,构造函数必须执行:ConstRet::ConstRet(int ii):i(ii),ci(ii),ri(i){ }
      • 总结一下就是:如果成员是const或者是引用,或者属于某种未提供默认构造函数的类类型,那么必须通过构造函数的初始值列表为这些成员提供初始值;
      • 强烈建议使用构造函数的初始化列表进行初始化工作,可以避免很多的错误;
      • 成员的初始化顺序和在类定义中出现的顺序是一致的,初始值列表中的不会影响实际的初始化顺序;
      • 需要令构造函数的初始值顺序和成员声明的顺序一致;防止初始化顺序出错;
      • 如果一个构造函数为所有的参数都提供了默认实参,则它实际上也定了默认构造函数;
      • 委托构造函数
      • 委托构造函数使用所属类的其他构造函数来执行自己的初始化过程,或者说他把自己的一些职责委托给其他构造函数;
      • 前提是这个类里面已经有构造函数,然后通过调用这个构造函数的方式完成构造函数的初始化工作; test.cpp;
    • 默认构造函数的作用
      • 当对象被默认初始化或者值初始化时,自动执行默认构造函数;
      • 默认初始化:
        • 1.当在块作用域内不适用任何初始值定义一个非静态变量或者数组时;
        • 2.当一个类本身含有类类型成员且使用合成的构造函数时;
        • 3.当类类型的成员没有在构造函数初始值列表中显示的初始化时;
      • 进行值初始化:
        • 1.在数组的初始化过程中,我们提供的初始值少于数组的大小时;
        • 2.当我们不使用初始值定义一个局部静态变量时;
        • 3.当我们通过书写形如T()的表达式,显示的请求值初始化时;
      • 在实际情况中,如果定义了其他构造函数,那么最好也提供一个默认的构造函数;
      • 对于定义的函数来说使不能使用成员访问运算符的;
    • 隐式的类类型转换

      • C++语言支持默认的集中内置类型的转换规则,同样的类也存在隐式的类类型转换规则,如果构造函数只有一个形参,那么就定义了转换为此类类型的隐式
        转换机制;
      • 可以通过一个实参调用的构造函数定义一条从构造函数的参数类型向类类型隐式转换的规则.

        class Sales_data{    public:        Sales_data(std::string s,unsigned cnt,double price):bookNo(s),units_sold(cnt),revenue(cnt*price){}        Sales_data():Sales_data("",0,0){}        Sales_data(std::string s):Sales_data(s,0,0);        Sales(std::istream, &is):Sales_data(){read(is,*this)}};
      • 隐式的执行转换:string null_book = "999-999-99",通过隐式的执行转换可以创建一个临时的Sales_data对象,然后会将对象里面的units_sold
        初始化为0,revenue初始化为0;但是如果希望执行item.combine("99-99-9999"),因为这样是希望执行一步转换的,就会失效;被创建的临时对象在一次
        使用之后就会被销毁;

      • 类类型转换只能够是一步进行类型转换,多部转换就会失效;
      • 如果希望阻止构造函数的隐式转换,需要使用关键字explicit来阻止这种隐式转换;
      class Sales_data{    public:        Sales_data(std::string s,unsigned cnt,double price):bookNo(s),units_sold(cnt),revenue(cnt*price){}        explict Sales_data():Sales_data("",0,0){}        explict Sales_data(std::string s):Sales_data(s,0,0);        explict Sales(std::istream, &is):Sales_data(){read(is,*this)}};
      • 使用关键字explict来禁止构造函数之间的隐式转换,上述需要进行一步隐式转换的代码就会出现错误;
      • explicit只允许出现在函数的声明处;
      • explict只能够用于直接初始化,但是不能够用于拷贝形式的初始化(也就是使用=)进行初始化的形式;
      • 当使用explict关键字声明构造函数时,直接以直接初始化形式进行使用,,并且编译器不会在自动转换过程中使用该构造函数;
    • 构造函数的显式的进行转换
      • 一种形式:

        item.combine(Sales_data(null_book));//用于显式的构造`Sales_data`对象;
        item.combine(static_cast<Sales_data>(cin));//执行的是显式的转换,并且也构造了临时的对象;
      • 虽然可以使用explict关键字来禁止隐式的自动转换,但是可以通过显式的转换使声明的过程变得合法;
      • 标准库中含有单参数的构造函数:
        • 1.接受单参数的const char*string构造函数不是explict;
        • 2.接受一个容量的vector构造函数是explict的;
  • 聚合类
    • 聚合类使得用户可以直接访问其成员;聚合类需要满足下面的条件:
    • 1.所有成员都是public的;
    • 2.没有定义任何构造函数;
    • 3.没有类内初始值;
    • 4.没有基类,没有虚函数;
    • 当聚合类需要进行初始化时,需要使用{},比如:Data Vail = {0,"Anna"};但是需要注意的是初始化的顺序必须和生命的顺序保持一致;
    • 如果初始值列表的数量少于类的成员数量,那么靠后的成员被初始化;
    • 初始值列表的元素个数不能够超过类的成员数量;
      关于聚合类需要注意的几点:
      • 1.要求所有的成员都是public的;
      • 2.需要用户完成初始化过程的控制;
      • 3.添加或者删除一个成员之后,所有的初始化语句都要更新;
  • 字面值常量类

    • 数据成员都是字面值类型的聚合类是字面值常量类;但是如果一个类不是聚合类,但是满足一下的要求,仍然是字面值常量类:
      • 1.数据成员都必须是字面值类型;
      • 2.类必须至少有一个是constexpr构造函数;
      • 3.如果一个数据成员含有类内初始值,内置类型的成员必须是一条常量表达式;
      • 4.或者如果成员属于某种类类型,则初始值必须定义自己的constexpr构造函数;
      • 5.类必须使用析构函数的默认定义;
    • constexpr构造函数
      • 1.构造函数不能够是const的,但是字面值常量类的构造函数至少提供一个constexpr构造函数;
        //TODO 268页;
  • 类的静态成员

    • 1.类里面的某些成员只需要和类本身相关,而没有必须为类的每个对象创建一个成员,这是就可以使用类的静态成员;

      class Account{    public:        void caculate(){amount += amount * interrestRate}        static double rate(){return interestRate}        static void rate(double){};    private:        std::string owner;        double amount;        static double interfaceRate;        static double initRate();};
    • 类的静态成员存在于任何对象之外,在对象中不包含任何的数据成员有关的数据.

    • 静态成员函数不能够和任何对象绑定在一起,并且不包含this指针;
    • 类的静态成员可以使用类的对象,引用或者指针来进行访问;
    • 成员函数可以不通过作用于运算符就能够直接访问静态成员;
    • 定义静态成员
      • 静态成员函数可以定义在类的外部也可以定义在类的内部;
      • 当在类的外部定义静态成员时,不能够重复使用static关键字,这个关键字只能够出现再类里面的声明语句中;
      • 静态数据成员不属于任何一个对象,所以不使用类的构造函数进行初始化;
      • 不能够类的里面初始化静态成员,必须在类的定义和初始化每个静态成员;
      • 一个静态数据成员只能够被定义一次;
原创粉丝点击