7类

一、类的简介:

  类的基本思想是抽象数据data abstraction)和封装encapsulation).数据抽象是依赖于接口(interface)和实现(implementation)分离的编程范式。类的接口包括所有用户能执行的操作,类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

  封装实现了类的接口和实现的分离


二、成员函数:

  成员函数必须在类内声明,但可以在类内部或外部定义。成员函数是所有对象共享的,而数据是每个对象独有的,即数据独有,成员共享

成员函数通过一个名为this的额外隐式参数来访问调用它的那个对象,this是一个常量指针(不能再指向其他的地址,但是可以通过指针改变所指的对象的值)。即this是存在于成员函数内部的一个隐式参数。

  通俗介绍下this是什么?当我们进入一个房子可以看到房子内的椅子、桌子、柜子等,但是看不到房子的全貌。对一个类的实例来说,你可以看到它的成员函数、数据成员,但是看不到该实例本身,this是一个指针,它时刻指向这个实例本身。

  当我们调用一个成员函数时,用请求该函数的对象地址来初始化this,即将this这个常量指针指向这个对象的地址,从此再也不变。(山无棱、天地合)

total.isbn()   //编译器把total的地址传给isbn的隐式形参this
改写成如下形式:
Sales_data.isbn(&total) //调用Sales_data的isbn成员时,传入了total的地址。

  在类的内部,我们可以直接使用对象的成员函数调用该对象的数据成员,不用使用成员访问符,因为this正是指向这个对象。任何对类成员的直接访问都被看做是this的隐式引用。也就是说当isbn使用bookNo时,它实际使用的是this指向的对象,即隐式返回this.bookNo,就如同this->bookNo.


三、常量成员函数(const member function):

  默认情况下,this的类型是指向非常量版本类类型常量指针所以我们不能把this绑定到一个常量对象上例如:在Sales_data的成员函数中,this的类型是Sales_data *cost。 this指针是一个指向非常量类的指针,当然不能用常量对象初始化它,要想存放常量对象的地址,必须使用指向常量对象的指针。

  这一情况也就使得我们不能在常量对象上调用普通的成员函数,因为成员函数内部有一个this指针,它是指向调用对象的地址的,但是this是一个指向非常量的常量指针,它不能被常量对象初始化。

  所以我们应该把this声明成一个指向常量的常量指针,即const Sales_data *const .但是this是成员函数的隐式参数,并不会出现在参数列表中,所以C++选择把const放在参数列表之后,指明this是一个指向常量的常量指针。

  const的作用是修改隐式this指针的类型,像这样使用const的成员函数被称为常量成员函数(const member function).经过修改,this是一个指向常量成员的常量指针,所以它既不能修改指向的地址,也不能通过this指针改变它所指向的内容的值。

  常量对象、常量对象的指针或引用都只能使用常量成员函数。


四、类的作用域和成员函数:

  编译器分为两步:首先编译成员的声明,再轮到成员的函数体。因此成员函数可以任意使用类中的其他数据成员而无须在意成员出现的次序。

如果在类的外部定义成员函数,外部定义必须与内部声明相匹配,即返回类型、参数列表和函数名、以及const属性都要一样。并且要采用作用域运算符来说明:剩余的代码是位于类的作用域内的,在函数体内使用数据成员时,实际上它是在隐式的使用类的数据成员。

1 double ::avg_price() const {  //使用作用域运算符,强调以下代码是位于类的作用域内部的
2     if(units_sold)
3       return revenue/units_sold;   
4 
5 }

五、返回一个*this对象的函数:

  

Sales_data & Sales_data ::combine(const Sales_data &rhs)
{
    units_sold += rhs.units_sold; //将rhs的成员加到this对象的成员上
    revenue += rhs.revenue;
    return  *this;  
}
total.combine(trans); //更新total的当前值 rhs绑定到trans上,total的地址被隐式绑定到this上。

上述这个代码非常有意思,当我们定义的函数类似于某个内置运算符时,应该令该函数尽量模仿这个内置运算符。内置运算符把她的左侧对象当成左值返回,所以此时左侧对象是一个Sales_data(即total),则返回对象应该是Sales_data &;

return *this; // return语句解引用this指针获得执行该函数的对象total,返回total的引用。

  只有返回的是total对象的引用才能确保操作是在total对象上进行的,如果返回的是total而不是total&,则combine函数返回的将是*this的副本。


六、从const成员函数返回*this:

    一个const成员函数如果以引用的形式返回*this,那么它的返回类型是常量引用。因为此时*this是一个指向常量的常量指针,此时返回*this,则是一个常量引用。

   我们在一个const object上只能调用const member function, 

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;}
private:
  void do_display(std::ostream &os) const {os << contents;}
};

基于函数重载,当给display函数传入一个非常量时,它的this指针将从指


class


1.类成员介绍
  • private: 私有数据成员和成员函数
  • protected: 受保护的数据成员和成员函数
  • public: 公有成员和成员函数
2.访问权限
  • public:可以被类外任何程序段访问,作为类被外部访问的接口。
  • private:只能够被类内部的公有成员和友元类成员访问,其他类的成员函数、派生类的成员函数、该类的对象均不可以访问。
  • protected:在没有继承的情况下,protected跟private相同。在派生类的时候才出现分化。基类对象不能访问基类的protected成员,派生类(不是对象)中可以访问基类的protected成员。

3.关于类定义的几点说明

  • 类体的命名符合标识符的命名规则。
  • 类体必须用一对花括号括起来,并且定义完成之后以分号结束。
  • 类体中的三个访问权限顺序是任意的,若把私有权限放在类的最开始,那么private可以省略。
  • 类中的数据不允许被初始化,类毕竟在某种程度上可以理解为一种特殊的数据类型.
  • 在定义类时,系统并未分配内存空间,在定义类的对象时才会分配内存单元。

构造函数


  • 构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

  • 不同的构造函数必须在参数数量或参数类型上有所区别。
  • 构造函数不能被声明成const的。
  • 编译器会隐式定义一个默认构造函数。

> 某些类不能依赖于合成的默认构造函数:
  • 只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。
  • 对某些类来讲,合成的默认构造函数可能执行错误的操作。
  • 有时编译器不能为某些类合成默认构造函数。
构造函数初始值列表
  • 负责为新创建的对象的一个或几个数据成员赋初值。
  • 当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐初始化。
  • 当构造函数的唯一目的是为数据成员赋初值,函数体为空。
  • 如果成员是const、引用、或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员提供初值。
类的外部定义的构造函数
  • 当在类的外部指定构造函数时,必须指明该构造函数是哪个类的成员。
委托构造函数

委托构造函数也有一个成员初始值列表和一个函数体。


## 默认构造函数

---
面向对象的程序设计语言倾向于对象一定要经过初始化后,使用起来才比较安全,因此,引入了构造函数的概念,用于对对象进行自动初始化。

##### 1.构造函数
构造函数是一种特殊的类成员函数,是当创建一个类的对象时,它被调用来对类的数据成员进行初始化和分配内存。(构造函数的命名必须和类名完全相同)。构造函数初始化对象的非static数据成员;
首先说一下一个C++的空类,编译器会加入哪些默认的成员函数

- 默认构造函数和拷贝构造函数
- 析构函数
- 赋值函数(赋值运算符)
- 取值函数
> 即使程序没定义任何成员,编译器也会插入以上的函数! 
> 注意:构造函数可以被重载,可以多个,可以带参数;
> 析构函数只有一个,不能被重载,不带参数

---
在一个构造函数中,*成员的初始化是在函数体之前完成的*,**且按照它们在类中出现的顺序进行初始化**。构造函数执行时,对象的内存空间已经分配好了,构造函数的作用是初始化这片空间。


---
#### 2.无参构造函数和默认构造函数
无参构造函数,不论是编译器自动生成的,还是程序员写的,都称为默认构造函数(defaultconstructor)。如果编写了构造函数,那么编译器就不会自动生成默认构造闲数。


而默认构造函数没有参数,它什么也不做。当没有重载无参构造函数时,
A a就是通过默认构造函数来创建一个对象


### 构造函数的初始化值列表
---
 C++初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。

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

} //初始化值列表
Sales_data(): Sales_data("",0,0) {} // 默认构造函数委托给了三参数的版本。 Sales_data(std::string s): Sales_data(s,0,0) {} // 接受一个string参数的构造函数也将其委托给了三参数版本。 Sales_data(std::istream &is):Sales_data() {read(is,*this);} //接受istream参数的构造函数,先委托给默认构造函数,然后默认构造函数再委托给三参数的构造函数。




7.2 访问控制与封装

先看下封装有什么好处:

  1.一旦数据成员被定义成private,类的作者就可以自由修改数据,只要类的接口不变(public),用户代码就无需改变。

  2.防止由于用户的原因造成数据被破坏,因为用户无法直接访问到private成员,所以当对象状态被破坏时,只有实现部分的代码采可能产生这样的错误,找bug会大大节省时间。


一、使用public和private封装:

  在使用访问说明符之前,user可以直接到达类对象的内部并且控制它的具体实现细节:我们需要使用访问说明符(access specifiers)来对类进行封装,使类的成员受到保护,想让谁看谁才能看到。

  • 用public说明符修饰的成员在整个程序内都是可以被访问的,public成员定义的是类的接口。
  • 用private说明符修饰的成员只能被类的成员函数访问,但是不能被类外的代码访问。private封装了部分实现细节。

一个类可以包含0或多个访问说明符,对于某个访问说明符能出现多少次也没有严格规定。


二、class和struct的区别:

其实这俩本来没啥区别,都是C++为了向下兼容C,唯一的区别就是:使用struct关键字定义的类,在第一个访问说明符之前的成员是public的;使用class定义的类在第一个访问说明符之前的成员是private的。


三、友元:

  类可以允许其他类或者函数访问它的非公有成员,方法是令其他class或者function成为它的友元(friend)。wow,你可以达到堂屋门哦!好感人啊,别人不行,只有friend才可以。

  友元声明只能出现在类定义的内部(仅仅指定访问权限),但是在类内出现的具体位置不限定,友元不是类的成员,也不受到它所在区域访问控制级别(public/private)的约束。(这些约束是对外来人的,你是我的friend,对你无效哦),不过一般来说,还是在类定义开始或结束前的位置集中声明友元。

  友元在类内的声明仅仅指定了访问权限,而不是一个真正的函数声明,所以我们需要在类外对函数再进行一次声明(使友元对类的用户可见)。为了使友元对类的用户可见,我们通常把对友元的声明与类本身放在同一个头文件中(类的外部)。

   

  此外类也可以将其他类定义成友元,也可以把其他类的成员函数定义成友元。

  

1 class Screen{
2   friend class Window_mgr;
3 
4 };

  将Window_mgr指定为友元类,则Window_mgr的成员函数可以访问Screen的所有成员(public和private等所有)。

此外还可以将一个成员函数声明为友元:

  

1 class Screen{
2    //Window_mgr::clear必须在Screen类之前被声明
3    friend void Window_mgr::clear(ScreenIndex);
4 
5 };

 先定义Window_mgr,在Window_mgr的定义里声明clear函数,然后再定义Screen类,在Screen类中包含对clear的声明,最后再定义clear函数,此时才能使用Screen的成员。


 四、在类中定义类型别名:

除了在类中定义数据和函数成员以外,类还可以自定义某种类型在类中的别名。类型别名也需要用访问说明符修饰。

  如下所示:

1 class Screen{
2 public:
3    //使用类型别名
4    typedef std::string::size_type pos;       
5    //也可以使用别名声明
6   using pos = std::string::size_type;
7 };

 定义类型的成员必须先定义才能使用,这与普通成员有所区别(原因后面再填坑),所以类型成员通常出现在类开始的地方。

 在类内使用类型别名有什么好处呢? Screen的用户不知道Screen使用了什么类型来存储它的数据,所以通过把pos定义成public成员来隐藏Screen实现的细节。


 五、将成员设置为inline函数

  定义在类内部的函数自动成为inline的(注意是定义!!!)。我们最好在类的外部用inline说明符修饰函数,使它成为内联的。不过也可以在类的内部把inline作为声明的一部分显式的声明成员函数(不推荐)


六、可变数据成员(mutable data member)

  有时我们希望修改类的某个数据成员,可以在变量声明前加入mutable关键字。一个可变数据成员永远不可能是const的,即使它是在一个consts对象内。因此一个const成员函数可以修改一个mutable成员的值。任何成员函数包括const函数在内的所有函数都可以修改它的值。

  可变数据成员的作用是什么呢? 比如你让给一个const成员函数传入(const class object)此时你就不能更改class object的值,但是你恰巧需要一个变量是变动的,此时最能满足你的就是mutable data member。

class Screen{
public:  
   void some_member() const;
private:
    mutable size_t access_ctr;  //即使在一个const对象内也能够被修改
 
};

void Screen::some_member() const
{
   ++access_ctr; 
}

七、类数据成员的初始值:

  当我们初始化类类型的成员时,需要为构造函数传递一符合成员类型的实参。最好的方式就是把这个默认值声明成一个类内初始值。

如下所示:

1 class Window_mgr{
2 private:
3    std::vector<Screen> screens{Screen(24,80,'  ')}; 
4 
5 };

7.4 类的作用域

 每个类都会定义它自己的作用域。在类的作用域之内,普通的数据和函数成员只能由对象、引用、或指针使用成员访问符来访问。对于类类型成员,则使用作用域运算符访问。


一、作用域

  一个类就是一个作用域,当我们在类的外部定义成员函数时必须提供类名和函数名,一旦遇到类名,定义的剩余部分就在类的作用域之内了,剩余部分包括参数列表和函数体。

1 void Window_mgr::clear(ScreenIndex i)
2 {  
3   Screen &s = screens[i];
4   s.contents = string(s.height *s.width,' ');
5 
6 }

 上述代码,当遇到Window_mgr后,就进入了类的作用域了,所以不再专门说明ScreenIndex是Window_mgr定义的。


二、类外部的函数定义:具有返回值时

 1 class Window_mgr{
 2 public:
 3    //向窗口添加一个Screen返回它的编号
 4    ScreenIndex addScreen(const Screen&)
 5 };
 6 //在类外定义时,返回值还没有进入类的作用域中,必须指明它是哪个类的成员
 7 Window_mgr ::ScreenIndex 
 8 Window_mgr ::addScreen(const Screen &s)
 9 {
10     screens.push_back(s);
11     return screens.size() -1;
12 }

7.5 类的构造函数

构造函数:类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。其任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

一、先看下构造函数的特点:

  构造函数没有返回类型,可能有一个参数列表(可能为空)和一个函数体(可能为空)。构造函数不能被声明成const,因为当我们创建一个const对象时,直到构造函数完成初始化过程,对象才真正能取得“const”属性。

合成的默认构造函数将会按照如下规则初始化数据成员:1.如果存在类内初始值,则用它来初始化成员。2.否则默认初始化该成员。

二、有些类不能依赖于合成默认构造函数

  • 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。一旦我们定义了其他构造函数(不是默认构造函数),除非我们再定义一个默认构造函数,否则类将没有默认构造函数。(和发救济粮一样,能吃上白面时,政府不会给你发窝窝头了,想吃窝窝头就得自己做)。
  • 合成的默认构造函数可能引发错误,如果类包含有内置类型或者符合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合使用合成的默认构造函数。
  • 有时编译器不能为某些类合成默认构造函数。比如如果类中包含一个其他类类型的成员并且这个成员的类型没有默认构造函数
 1  1 struct  Sales_data{
 2  2 
 3  3 //默认构造函数 在参数列表后面加上=default来要求编译器生成构造函数。如果=default出现在类内部,则默认构造函数是内联的,否则不是。该构造函数之所以有效,是因为我们为内置数据成员提供了初始值。
 4  4    Sales_data()  = default; 
 5  5    Sales_data(const std::string &s):bokNo(s) {}
 6  6    //冒号及冒号之间的部分称为构造函数初始值列表,括号内称为参数列表,花括号内称为函数体。
 7  7    Sales_data(const std::string &s,unsigned n,double p ):bookNo(s),units_sold(n),revenue(p*n){}
 8  8     Sales_data(std::istream &);
 9  9 
10 10      //string是一个类,它决定自己的对象的初始值是什么,如果没有指定初始值,则生成一个空串。 
11 11      std::string bookNO;
12 12      //在内部的内置类型应该指定类内初始值
13 13     unsigned units_sold = 0;
14 14     //且构造函数不应该轻易覆盖掉类内初始值,除非新赋的值与原值不同。
15 15     double revenue =  0.0;
16 16 
17 17 };
18 18 //我们定义Sales_data类的成员,它的名字是Sales_dtata.这个构造函数初始值列表是空的,但是由于执行了构造函数体,对象的成员仍然能够被初始化。通过相应的类内初始值初始化。
19 19 Sales_data::Sales_data(std::istream &is){
20 20      read(is,*this);
21 21 

三、看下在构造函数内部成员是如何初始化的:

  如果没有在构造函数的初始值列表中显式的初始化成员,则该成员将在构造函数体之前执行默认初始化。

1 //这个是对数据成员进行了赋值操作,而不是初始化。=号是赋值操作。初始化的含义是创建变量时赋予其一个初始值,而赋予的含义是把对象的当前值擦除,而以一个新值来替代。
2 Sales_data::Sales_data(const string &s,unsigend cnt,double price){
3      bookNo = s;
4      units_sold = cnt;
5      revenue = cnt *price;   
6 
7 }

  如果成员是const是const或者是引用的话,必须将其初始化。随着构造函数体一开始执行,初始化就完成了。我们初始化const或者引用类型的唯一机会就是通过构造函数初始值。

 1 ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i){} 

四、再来看下什么是委托构造函数(就是把活儿都给人家干,嫁接在别的构造函数上):

  一个委托构造函数可以使用它所属类的其他构造函数来执行它自己的初始化过程。委托构造函数也有一个成员初始值列表和一个函数体。成员初始值列表只有一个唯一入口,就是类名本身

 1 class Sales_data{
 2 public:
 3     //非委托构造函数使用对应的实参初始化成员 
 4    Sales_data(std::string s,unsigend cnt ,double price):bookNo(s),units_sold(cnt),revenue(cnt*price){} 
 5 //其余构造函数全部委托给另一个构造函数
 6    Sales_data():Sales_data("",0,0){}
 7    Sales_dta(std::string s):Sales_data(s,0,0){}
 8   //先委托给默认构造函数,然后默认构造函数再委托给三实参函数。
 9    Sales_data(std::istream &is):Sales_data(){read(is ,*this);}
10 };

五、来关注下默认构造函数的作用:

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

  • 当我们在块作用域内不使用任何初始值定义一个非静态变量。
  • 当一个类本身还有类类型成员并且使用合成的默认构造函数时。
  • 当类类型成员没有再构造函数初始值列表中显示初始化时。

  值初始化发生在以下情况:

  • 在数组初始化过程中,如果我们提供的初始值数量少于数组大小时。
  • 当我们不使用初始值定义一个局部静态变量时
  • 通过书写形如T()的表达式显示请求值初始化时

六、来关注下隐式的类类型转换(只适用于提供一个实参的构造函数,生成一个临时类对象,传入到类的成员函数中)

转换构造函数:如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。

1 //在我们的Sales_data类中,接受string或者istream的构造函数分别定义了从这两种类型向Sales_data的隐式转换准则。也就是说在需要使用Sales_dta的地方可以使用这两种类型替代。但是只允许一步隐式转换。
2 string null_book = "9-9999-9999";
3 //构造一个临时Sales_data对象,该对象的bookNo=null_book,其他参数由类内初始值指定。因为combine的参数是常量引用,所以我们可以传递给它一个临时量。
4 item.combine(null_book);
5 
6 //下面的函数就是错误的,因为隐式类型转换只允许一步转换。
7 item.combine("9-9999-9999")
8 //不过我们可以显示的将字符串转换为Sales_data,这个Sales_data对象就是个临时量。
9 item.combine(Sales_data(9-9999-9999)); 

抑制隐式转换:

 1 class Sales_data{
 2 public:
 3         Sales_data() = default;
 4         //需要多个实参的构造函数不能用于执行隐式转换,所以无需将其指定为explicit
 5         Sales_data(const std:string &s,unsigend n,double p):
 6            bookNo(s),units_sold(n),revenue(p*n){}
 7 //虽然不能隐式转换了,但是可以显式转换
 8         explicit Sales_data(const std::string &s) :bookNo(s) {}
 9         explicit Sales_data(std::istream&) ;
10 
11 }
12 
13 item.combine(null_book); //错误:string构造函数是explicit
14 Sales_data iterm1(null_book);   //正确:直接初始化 可以使用explicit类型的构造函数
15 Sales_data iterm2 =null_book; //错误:不能将explicit构造函数用于拷贝形式的初始化
16 //虽然执行了explicit构造函数,但是仍然可以执行显示地强制进行转换
17 //static_cast可以使用explicit的构造函数
18 item。combine(static<Sales_data>(cin))

  

陈小洁的三只猫
原文地址:https://www.cnblogs.com/ccpang/p/11362698.html