C++Primer学习笔记:第7章 类

  • 类的基本思想是数据抽象data abstraction和封装encapsulation。数据抽象是一种依赖于接口interface和实现implementation分离的编程技术

  • 在类中,由类的设计者负责考虑类的实现过程,使用该类的程序员只需要抽象地思考类型做了什么,而无须了解类型的工作细节

  • 执行加法和IO的函数应该定义成普通函数,执行复合赋值运算的函数应该是成员函数

  • 成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部

  • 成员函数通过一个名为this的额外的隐式参数来访问调用它的对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化该常量指针this。因此我们不能自定义名为this的参数或者变量

  • 紧随参数列表之后的const关键字修改隐式this指针为指向类类型常量版本的常量指针(默认不是指向常量的)。这意味着如果一个成员函数是普通函数(函数参数列表后没有const),则一个常量类对象无法调用该函数(无法给this指针初始化)

  • 如果成员函数内部对对象的其他成员没有修改,则尽可能将成员函数声明为常量成员函数,紧跟在函数参数列表后面的const表示this是一个指向常量的常量指针。将函数声明为常量成员函数有利于提高函数的灵活性,令常量对象仍然可以调用该函数

  • 常量对象,以及常量对象的引用或者指针都只能调用常量成员函数

  • 类本身就是一个作用域,编译器分两部处理:首先编译成员的声明,然后才轮到成员函数体。成员函数体可以随意使用类中的成员而无须在意这些成员出现的次序

  • 类外部定义的成员的名字必须包含它所属的类名,而且返回类型、参数列表和函数名都得与类内部的声明保持一致

    double Sales_data::avg_price() const 
    {
    
    }
    
  • 当我们定义函数类似于某个内置运算符时,应该令函数的行为尽量模仿这个运算符

  • 如果函数在概念上属于类但是不定义在类中,则它一般应该与类声明在同一个头文件中

  • 默认情况下,拷贝类的对象其实是拷贝对象的数据成员

  • 构造函数没有返回类型,构造函数不能被声明为const,当我们创建类的一个const对象时,直到构造函数完成初始化过程对象才真正取得常量属性

  • 如果我们的类没有显式地定义构造函数,则编译器会为我们隐式定义一个默认构造函数。默认构造函数没有任何实际参数。这个编译器创建的构造函数又称作合成的默认构造函数

    • 如果存在类内的初始值,用它来初始化成员
    • 否则,默认初始化该成员
  • 不能依赖默认构造函数:

    • 一旦我们定义了构造函数(不论是否有参),除非我们自己定义一个默认构造函数(无参),否则类将没有默认构造函数。只有当类没有声明任何构造函数的时候编译器才会自动生成默认构造函数
    • 如果定义在块中的内置类型或复合类型(数组和指针)的对象被默认初始化,则其值是未定义的。除非他们都有类内初始值
    • 如果类中包含一个其他类,而且这个类没有默认构造函数,那么编译器将无法初始化该成员
  • 如果函数定义在类内部,则函数默认是内联的,如果在外部,则默认是不内联的

  • 在C++11新标准中,我们可以通过在参数列表后面写上=default来要求编译器生成构造函数(在类外仍然可以)

  • 在构造函数中可以使用构造函数初始值列表对部分成员进行初始化,在初始值列表初始化后用类内初始化对成员进行初始化,剩下的执行默认初始化,然后再执行构造函数函数体中的内容

  • 如果我们不定义拷贝、赋值和析构操作,则编译器会会替我们合成。一般来说,编译器生成的版本将对对象中的每个成员执行拷贝、赋值和销毁操作

    • 拷贝:初始化变量、以值的方式传递或返回一个对象
    • 赋值:使用赋值运算符
    • 销毁:当对象不再存在时销毁
  • 每个访问说明符指定了接下来成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾为止

    • 定义在public说明符之后的成员在整个程序内可以被访问,定义类的接口
    • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问
  • classstruct的唯一区别:默认访问权限不同,struct的默认访问权限是publicclass的默认访问权限是private

  • 类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。友元声明只能出现在类定义的内部,但是具体位置不限。一般来说,最好在类定义开始或结束前的位置集中声明友元

  • 友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明,我们必须在友元声明之外再专门对函数进行一次声明(一些编译器允许不再次声明,不过为了能够让所有的编译器成功运行,最好还是加上)。为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中。

  • 除了定义数据和函数成员外,类还可以定义某种类型在类中的别名,由类定义的类型名字和其他成员一样存在访问限制

    class Screen 
    {
    public:
        typedef std::string::size_type pos;
        //using pos = std::string::size;
    }
    

    用来定义类型的成员必须先定义后使用,这一点与普通成员有所区别。因此类型成员通常出现在类开始的地方

  • 如果我们希望无论如何都能够修改某个类的数据成员,即使该对象是const,我们可以在变量的声明中加入mutable关键字,称作可变数据成员。可以推测,一个可变数据成员无论如何都不是const的。

  • 当我们提供类内初始值时,必须以符号=或者花括号表示,不能用圆括号

  • 如果想要在成员函数中返回对象本身,则返回类型应该为引用,返回值为*this

  • 一个const成员函数如果以引用的形式返回*this,则应该返回一个常量引用

  • 我们可以通过成员函数是否是const对函数进行重载,其原因如同函数的指针参数是否指向const可以进行重载一样。如果一个函数既有const版本,又有非const版本,则对于常量对象仅仅可以调用const版本,对于非常量对象非const版本显然是一个更好的匹配

  • 在类内使用一些小函数不会增加运行时的额外开销,相反还会给开发带来很多好处

    class Screen
    {
        public:
        Screen &display(std::ostream &os) {
            do_display(os); return *this;
        }
        const Screen &display(std::ostream &os) const {
            do_display(os); return *this;
        }
        void do_display(std::ostream &os) const {
            os << contents;
        }
    }
    
  • 每个类定义了唯一的类型,对于两个类来说,即使他们的成语完全一样,这两个类也是完全不同的类型

  • 在C语言中要求类类型定义对象时加上classstruct,但是C++中不必要

  • 我们也可以仅仅声明类而不定义它,这种声明有时被称作前向声明,在定义之前是一个不完全类型:可以定义指向这种类型的指针或者引用,也可以声明(不能定义)以不完全类型作为参数或者返回类型的函数。因此,一个类的名字出现过以后,就被认为是声明过了,允许包含指向它自身类型的引用或者指针

  • 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。友元关系不具有传递性

  • 需要注意的是,友元的声明仅仅影响的是该函数的访问权限,因此并不能看作声明,为了完成对该友元的调用,必须在其作用域内对友元进行声明。友元的作用域和类的声明是同一级别的

    struct X
    {
        friend void f()	{}	//友元函数可以定义在类的内部,但是此时在类X的作用域中是不可见的
        X() { f(); }		//错误,f()不可见
        void g();
        void h();
    }
    void X::g() { f(); }	//错误,f()不可见
    void f();				//对f进行声明,此时对X可见
    void X::h()	{ f(); }	//正确
    

    需要注意的是,有的编译器对上面没有这种限制

  • 定义在外部的成员函数的返回类型不在类的作用域内

  • 类的定义分两步处理:

    • 顺序编译成员的声明
    • 指导类全部可见后编译函数体
  • 一般来说,内层作用域可以重新定义外层作用域名字,即使该名字已经在内层作用域中使用过,而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表某一种类型,则类不能重新定义该名字(一些编译器忽略这种错误)

  • 类型名的定义通常出现在类的开始处,这样保证所有使用该类型的成员都出现在类型之后

  • 尽管全局变量有可能被覆盖掉,但是我们可以使用::name来获取对应的全局变量

  • 当成员定义在类的外部时,名字查找的最后一步不仅要考虑类定义之前的全局作用域中的声明,还要考虑在成员函数定义之前的全局作用域中的声明

  • 如果没有在构造函数的初始值列表中显式地初始化成员,而且该成员没有类内初始值,则该成员将在构造函数体之前执行默认初始化。有时候有的变量无法进行默认初始化(如一些没有默认构造函数的类对象,以及引用或者常量)

  • 在构造函数初始值中每个成员只能出现一次。构造函数初始值列表只用于说明初始化成员的值,而不限定初始化具体执行顺序,成员的初始化顺序与他们在类定义中的出现顺序一致,构造函数初始值列表中初始值的前后位置不会影响实际的初始化顺序。如果使用某些成员初始化其他成员,则初始化的顺序就比较重要,不过这种做法不是一个好的编程习惯

  • 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数

  • 我们可以使用委托构造函数,委托其他构造函数进行构造

    class X
    {
        X(int _a, int _b, int _c):a(_a),b(_b),c(_c){}
        X():X(0, 0, 0){}
        X(int _a):X(_a, 0, 0){}
        X{int _a, int _b):X(_a, _b, 0){}
    };
    

    委托构造函数会先执行被委托构造函数的初始化列表,然后再执行函数体

  • 当对象被默认初始化或值初始化时自动执行默认构造函数

  • 默认初始化在以下情况下发生:

    • 当我们在块作用域内不适用任何初始值定义的一个非静态变量或者数组时
    • 当一个类本身含有类类型的成员且使用合成的默认构造函数时
    • 当类类型的成员没有在构造函数初始值列表中显式地初始化时
  • 值初始化在以下情况下发生:

    • 在数组初始化的过程中如果我们提供的初始值的数量小于数组的大小时
    • 当我们不使用初始值定义一个局部静态变量时
    • 当我们通过书写形如t()的表达式显式地请求值初始化时
  • 在实际中,如果提供了其他构造函数,最好也提供一个默认的构造函数

  • 如果想定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉对象名后的空的括号对

    sales_data obj();    //声明了一个函数
    sales_data obj1;     //声明了一个对象,使用默认构造函数进行初始化
    
  • 能通过一个实参调用的构造函数定义了一条从构造函数参数类型向类类型隐式转换的规则,但是这种转换只允许一步

  • 我们可以通过将构造函数声明为explicit阻止隐式转换,只对一个实参的构造函数有效。只能在类内声明构造函数时使用explicit关键字,在类外定义时不应重复。explicit构造函数只能用于直接初始化

  • 我们可以使用explicit构造函数进行强制类型转换

    item.combine(static_cast<sales_data>(cin));
    
    • const char * -> string的构造函数不是explicit
    • 接收一个容量参数的vector构造函数是explicit
    • 需要注意的是**explicit阻止的隐式转换是构造函数参数类型转换为类类型的转换**,而不是构造函数参数之间的转换
    • 允许临时量存在的地方才允许隐式类型转换
    • 聚合类使得用户可以直接访问其成员,因此具有特殊的初始化语法形式,当一个类满足如下条件时,我们说它是聚合的
      • 所有成员都是public
      • 没有定义任何构造函数
      • 没有类内初始值
      • 没有基类,也没有virtual函数
        例如:
      struct Data
      {
          int i; string s;
      };
      Data v1 = {0, "1"};
      
      我们可以提供一个花括号括起来的成员初始值列表用来初始化,当然顺序要和声明顺序一致。
  • 数据成员那都是字面值类型的聚合类是字面值常量类。或者满足以下要求:

    • 数据成员必须是字面值类型
    • 类必须至少含有一个constexpr构造函数
    • 如果某个成员有类内初始值,内置类型必须是一条常量表达式,类类型必须使用自己的constexpr构造函数
    • 类必须使用析构函数的默认定义
  • 我们可以通过在成员的声明之前加上关键字static使得其与类关联在一起。类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。因为静态函数成员中没有this指针),静态成员啊还是那话不能声明成const,也不能调用非静态成员

  • 可以使用作用域运算符直接访问静态成员。虽然静态成员不属于某个对象,但是我们还是可以使用类的对象、引用或者指针访问静态成员。成员函数不通过作用域运算符就能直接访问静态成员

  • 静态成员的static关键字只能在类内部出现(同explicit),不能在类外重复使用

  • 静态成员不能由类的构造函数初始化,静态数据成员定义在任何函数之外,因此一旦被定义就存在于程序的整个生命周期中。一般来说,我们不能在类的内部初始化静态成员,相反的,必须在类的外部定义和初始化每个静态成员

  • 通常情况下,类的静态成员不应该在类的内部初始化。然而我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。这样的静态常量可以用在所有适合用于常量表达式的地方。即使该成员在类内已经初始化,还是应该在类外定义一下该成员,而且不能再指定一个初始值了。这样做的好处是能够在类的外部使用该静态成员。

    void func(const int &x)
    {
      cout << "x:" << x << endl;
    }
    struct A
    {
      static const int a;
      static constexpr int aa = 6;
      static vector<int> v;
    };
    vector<int> A::v(aa);
    const int A::a = 5;
    int main()
    {
      //const A a;
      //a.test();
      cout << A::v.size() << endl;                                                                                                                          
      func(A::a);
      return 0;
    }
    
    
  • 静态成员能够用于某些场景而普通成员不能:

    • 静态数据成员可以是不完全类型,特别的,静态数据成员可以是它所属的类类型,而非静态数据成员则收到限制,只能声明成它所属类的指针或引用
    • 静态成员和普通成员的另外一个区别是我们可以使用静态成员为默认实参
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值