《C++ Primer》读书笔记 第7章:类

来源:互联网 发布:数据分析专业 上海财经 编辑:程序博客网 时间:2024/06/11 11:27

7.1 定义抽象数据类型

定义成员函数

  成员函数必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。成员函数通过一个名为this的额外隐式参数来访问它的那个对象。所以所有的非static成员函数都有一个隐式参数this指针,this指针是一个指针常量,我们不允许改变this指针中保存的地址。而且该this指针是作为成员函数的第一个形参。

    class Test    {    publicvoid  add(int i){_i += i;}   //实际函数的形式是void add(Test *const this, int i);    private:        int _i = 0;    }   

  我们可以通过在成员函数的形参列表后增加一个const关键字,来将成员函数声明为一个常成员函数。常成员函数的实际意义其实是吧this的指针的类型从指向非常量对象的常指针,声明为指向常量对象的常指针。

    class Test    {    public:        void  add(int i) const {_i += i;}   //实际函数的形式是void add(const Test *const this, int i);    }   

  将this指针声明为一个指向常量对象的常指针,这样在成员函数中就不可以通过this指针改变此对象中的值。常量对象以及常量对象的指针或引用都只能调用对象的常量成员函数。(从函数形参匹配的角度容易理解此性质)

  编译器再编译类时,首先编译成员的声明,然后才编译成员函数的函数体,因此,成员函数体内可以随意使用类中的其他成员函数而无需在意这些成员出现的次序。
  另外在类外定义的函数形式必须要与类内声明函数的返回类型、参数列表、函数名完全匹配,所以如果类内声明了一个函数为常成员函数,那么在类外定义时也必须加上const声明。(这样才能使参数列表完全匹配)

定义类相关的非成员函数

  一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个同文件中。

构造函数

  类可以包含多个构造函数。构造函数不能声明为const,因为当我们创建类的一个const对象时,直到构造函数完成初始化过程中,对象才能真正取得其常量属性。

合成的默认构造函数

  无任何实参的构造函数就是默认构造函数。如果我们的类没有显示定义任何默认构造函数,那么编译器会为类合成一个默认构造函数。对于大多数类来说,这个合成的默认构造函数会按照如下规则初始化类的数据成员:

  • 如果存在类内的初始值,则用初始值初始化此数据成员
  • 如果不存在类内的初始值,则编译器将默认初始化该成员。(此时,该数据成员将是随机值)

=default

  一旦我们定义了其他的构造函数,那么编译器将不会再为类合成一个默认的构造函数。如果此时我们需要一个这样一个默认的构造函数,我们可以通过在参数列表之后加=default来要求编译器生成一个默认构造函数,这是c++11种新增的特性。
  当然我们也可以通过构造函数初始值列表来赋予数据变量初值,而且列表中的初值将覆盖类内的初始值。

class Test{public:    Test() = defalut;     //要求编译器合成一个默认构造函数    Test(double d, int i = 1):_d(d),_i(i){}  //private:    int _i = 0;    double _d = 3.14;};

拷贝赋值与析构

  除了定义类的构造函数外,类还需要控制拷贝、赋值和销毁对象时发生的行为。
  如果我们不定义这些行为,编译器将为我们控制这些行为,通常我们不应该依赖编译器默认合成的版本。

7.2 访问控制和封装

友元

类可以允许其他类或者函数访问他的非公有成员,方法是令其他类或者函数成为它的友元。
友元的声明使用friend关键字进行声明。

class Screen{    friend void test(Screen &s);    //声明一个全局函数为友元函数    friend test;                    //声明别的类为友元    friend void window_mgr::clear(Screen &s);    //声明其他类的函数成员为友元函数}

友元声明只能出现在类定义的内部,但是类内出现的位置不限,友元不是类的成员,所以其也不受访问控制符的限制。通常来说,最好在类的开头或者结尾集中声明友元,这样可以使类的结构清晰。
另外要注意的一点是,如果我们希望类的用户调用某个友元函数,那么我们必须在友元声明之外再专门对函数进行一次声明。要切记,友元声明不等同于函数声明

7.3 类的其他性质

定义类型成员

我们可以在类内定义一个类型成员,但是要注意,定义的类型成员必须先定义再使用,这一点与类的普通成员有所区别。所以通常把类型成员都定义在类的开头。

class Screen{public:    pos pos1;   //错误,此时pos还未定义,不可使用    using pos = std::string::size_type; //定义类型成员 pos    pos pos2;   //正确}

可变数据成员

一个可变数据成员永远不是const,即使它是const对象中的成员,它仍然可以被改变。可变数据成员用关键字mutable来声明,其位置与const一致。

class Screen{privatemutable size_t access_ctr;          //声明一个可变数据成员}

类数据成员的初始值

在c++11新标准中,最好的初始化方式就是把默认值声明为一个类内初始值。
类内初始值必须使用=的初始化形式或者花括号括起来的直接初始化形式。

class Test{public:    Test(int a) :_a(a){}    int _a = 0;         //使用=进行类内初始化};class Screen{public:    ...private:    std::vector<Test> tests{Test(1), Test(2)};  //使用一个花括号序列表来初始化一个vector成员,但是vs2013似乎尚不支持此特性    int *b[10] = {};    int *c[10]{};   //对于数组的初始化以上两种形式均可,这样所有指针会被初始化为nullptr};

友元再探

友元关系不具有传递性,每个类负责控制自己的友元类和友元函数。
如果一个类想把一组重载函数声明为它的友元,那么它需要对这组函数中的每一个函数都进行一次友元声明。

令另一个类的成员函数作为友元

当把另一个类的成员函数声明为友元时,我们必须指出该成员函数属于哪一个类:

class Screen {    friend void window_mgr::clear(ScreenIndex);};

要注意这样声明友元函数时的声明次序:

  • 首先我们应该先定义window_mgr类,在其中我们声明clear函数,但是我们不能定义它,因为此时ScreenIndex类还未被定义。
  • 然后我们定义ScreenIndex类,包含对于clear函数的友元声明。
  • 最后我们定义clear函数,此时它才可以使用ScreenIndex类中的成员。

关于友元函数的声明,要特别注意的是,友元声明仅仅只是影响访问权限,它本身并非普通意义的商的函数声明。所以当你声明了一个函数的友元关系时,你还必须另外有一个函数声明才可以在此类中使用此函数。

struct X{    friend void f();    x() {f();}      //错误,只有f的友元声明,而没有真正意义的函数声明,此时f()不可以被调用    void g();};void f();void X::g() {f();}  //正确,此时f()函数已经有声明,可以被使用

7.4 类的作用域

定义在类外部的成员

当一个类成员函数在类外部定义时,一旦遇到类名,定义的剩余部分就在类的作用域中了。

int Test::test(int i){...}      //在这个声明中,形参列表和函数体在类的作用域内,而返回值在类的作用域外

所以,当使用类自定义类型定义类的成员函数时,其作为形参列表不需要额外加作用域符号,而作为返回值则必须额外加上类名以及作用域符号。

class Test{public:    using TestType = int;    TestType test(TestType t);};Test::TestType Test::test(TestType t)               //必须在返回值前加Test::{}

名字查找与类的作用域

对于成员函数,编译器会在处理完类中所有的声明后才会处理成员函数的定义,所以,成员函数可以使用类中定义的任何名字。
而对于声明中使用的名字,包括返回值类型或者参数列表中使用的名字,都必须确保在使用前可见。
如果某个成员声明中使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。一般来说,内层作用域可以重定义外层作用域中使用的名字,但是在类中,如果类中的自定义类型名不允许使用与外层作用域中相同的名字。(有些编译器并不能检测出此错误,但这种用法是错误的,应该避免)

7.5 构造函数再探

构造函数初始值列表

如果成员是const或者引用的话,那么必须对其进行初始化。类似的如果某个类类型成员没有定义默认构造函数,也必须将这个成员初始化。
初始化的方式有使用初始化列表进行初始化,c++11中新增了一种初始化方式,就是使用类内初始值。

    class Test    {    public:        Test() = default;    private:        const double _d = 3.14;         //利用类内初始值为const成员赋初值    };    class Test    {    public:        Test(double d) :_d(d){}         //使用构造函数初始化表为const成员赋初值    private:        const double _d;    };    class Test    {    public:        Test(double d) = default;     private:        const double _d;                //错误,const的成员没有被初始化    };

如果成员是const、引用,或者属于某个未提供构造函数的类类型,我们必须通过构造函数初始化列表或者是类内初始值为这些成员初始化。

成员初始化的顺序

成员初始化的顺序与它们在类中的定义顺序一致,而与构造函数中初始值列表中初始值得前后位置关系无关。

委托构造函数

一个委托构造函数可以使用其所属类的其他构造函数来执行自己的初始化过程。

    class Sales_data    {    public:        Sales_data(std::string s, int cnt, double price) :_name(s), _isbn(cnt), _price(price){}        Sales_data() :Sales_data("", 0, 0.0){}              //委托构造函数        Sales_data(std::string s) :Sales_data(s, 0, 0){}    //委托构造函数,并定义了隐式转换    private:        std::string _name;        int _isbn;        double _price;    };

默认构造函数的使用

在实际中,如果我们定义了其他构造函数,那么我们最好也应该定义一个默认构造函数。
另外要注意默认构造函数的使用方法。

Sales_data obj();   //错误,实际上是定义了一个函数,函数名为obj,返回值为sales_dataSales_data obj;     //正确

隐式的类型转换

如果构造函数只接受一个实参,则它实际上定义了转换为此类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数。
另外我们要注意的一点是,编译器只能隐式执行一步类类型转换,多余一次的话编译器将报错。

    void combine(Sales_data &s);        //定义一个以Sale_data类型为形参的函数    combine(“9-99-9999”);       //错误,需要两次隐式类类型转换,const char * -> string -> Sales_data,编译器将报错    combine(Sales_data("9-99-9999");        //正确,显式进行第二次类类型

我们可以通过把构造函数声明为explicit来阻止隐式类型转换。

在c++11之前,explicit只能使用与含一个参数的构造函数,但是c++11之后其可以用于有任意参数个数的构造函数声明之中。在c++11中,定义了一个类类型到值序列表的隐式转换,而explicit可以限制这种隐式转换。比如Sales_data的类型中定义了一个含三个形参的构造函数,所以序列表
{string(“test”),0, 3.14}可以隐式转换为Sales_data类型。

7.6类的静态成员

我们可以在成员的声明之前加上关键字static使其称为一个静态成员,静态成员可以是public或者private。静态数据成员的类型可以是常量、引用、指针、类类型等。
静态成员不与任何对象绑定在一起,他们不包含this指针,所有对象共享一份静态成员。由于没有this指针,所以静态成员函数不能被声明为const成员,而且我们也不能在static成员函数中使用this指针。
我们不需要通过对象访问静态成员,而是可以通过作用域运算符直接访问静态成员,但是通过一个对象访问其静态成员函数也是合法的。

    double r = Account::rate();     //Account为类类型名,rate()为其静态成员函数

我们既可以在类的内部,也可以在类的外部定义类的静态成员函数,但是在类的外部定义静态成员是,不能重复static关键字,static关键字只能出现在类内部。
对于静态数据成员来说,因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的,这意味着它们不是由类的构造函数初始化的。一般来说,我们不能再类的内部初始化静态成员。相反的,我们应该在类的外部定义和初始化每个静态成员,而且一个静态数据成员只能被定义一次。为了确保静态数据成员只被定义一次,应该将其定义放在源文件中。

通常情况下,类的静态成员不能在类内初始化,但是如果静态数据成员是字面值常量类型的constexpr,那么其可以在类内使用类内初始值进行初始化,而且初始值必须是常量表达式。如果在类内提供了一个初始值,那么可以在类外定义此数据成员,但是不能再为其指定一个初始值了。一般来说,即使一个常量数据成员在类内部被初始化,我们也应该在类的外部定义一下此成员。

    class Sales_data    {        std::string _name;        int _isbn;        static int _test1 = 10;                 //错误,非constexpr字面值常量类型不能提供类内初始值        static const int _test2 = 10;           //正确,static的constexpr字面值常量可以提供类内初始值        static constexpr int _test3 = 10;       //正确,同上    };    const Sales_data::_test2 = 100;             //错误,类外不能再提供初始值    constexpr Sales_dat::_test3;                //正确

一些静态成员能使用而普通成员不能使用的情况

静态数据成员可以是不完全类型,特别的,静态数据成员的类型可以就是它所属的类类型。另外我们可以使用静态数据成员作为默认实参。

    class Screen    {    public:        Screen &clear(char c = _c);         //静态数据成员可以作为函数的默认参数    private:        static Screen s;        //可以是不完全类型,甚至可以是类本身        static char _c;                 };    Screen Screen::s = Screen();    char Screen::_c = 'a';
原创粉丝点击