类是 C++ 中最重要的特征。C++ 语言的早期版本被命名为“带类的 C(Cwith Classes)”,以强调类机制的中心作用。随着语言的演变,创建类的配套支持也在不断增加。语言设计的主要目标也变成提供这样一些特性:允许程序定义自己的类型,它们用起来与内置类型一样容易和直观。

类的定义和声明

  • 类背后蕴涵的基本思想是数据抽象封装
  • 数据抽象是一种依赖于接口和实现分离的编程(和设计)技术。类设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。相反,使用一个类型的程序员仅需了解类型的接口,他们可以抽象地考虑该类型做什么,而不必具体地考虑该类型如何工作。
  • 封装是一项低层次的元素组合起来的形成新的、高层次实体珠技术。函数是封装的一种形式:函数所执行的细节行为被封装在函数本身这个更大的实体中。被封装的元素隐藏了它们的实现细节——可以调用一个函数但不能访问它所执行的语句。同样地,类也是一个封装的实体:它代表若干成员的聚焦,大多数(良好设计的)类类型隐藏了实现该类型的成员。
  • 标准库类型 vector 同时具备数据抽象和封装的特性。在使用方面它是抽象的,只需考虑它的接口,即它能执行的操作。它又是封装的,因为我们既无法了解该类型如何表示的细节,也无法访问其任意的实现制品。另一方面,数组在概念上类似于 vector,但既不是抽象的,也不是封装的。可以通过访问存放数组的内存来直接操纵数组。
  • 并非所有类型都必须是抽象的。标准库中的 pair 类就是一个实用的、设计良好的具体类而不是抽象类。具体类会暴露而非隐藏其实现细节。一些类,例如 pair,确实没有抽象接口。pair 类型只是将两个数据成员捆绑成单个对象。在这种情况下,隐藏数据成员没有必要也没有明显的好处。在像 pair 这样的类中隐藏数据成员只会造成类型使用的复杂化。
  • 数据抽象和封装提供了两个重要优点:1.避免类内部出现无意的、可能破坏对象状态的用户级错误。2.随时间推移可以根据需求改变或缺陷(bug)报告来完美类实现,而无须改变用户级代码。

隐含的this指针

  • 成员函数具有一个附加的隐含形参,即指向该类对象的一个指针。这个隐含形参命名为 this,与调用成员函数的对象绑定在一起。成员函数不能定义 this 形参,而是由编译器隐含地定义。成员函数的函数体可以显式使用 this 指针,但不是必须这么做。如果对类成员的引用没有限定,编译器会将这种引用处理成通过 this 指针的引用。
  • 尽管在成员函数内部显式引用 this 通常是不必要的,但有一种情况下必须这样做:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用。
  • 在普通的非 const 成员函数中,this 的类型是一个指向类类型的 const 指针。可以改变 this 所指向的值,但不能改变 this 所保存的地址。在 const 成员函数中,this 的类型是一个指向 const 类类型对象的const 指针。既不能改变 this 所指向的对象,也不能改变 this 所保存的地址。注意不能从 const 成员函数返回指向类对象的普通引用。const 成员函数只能返回 *this 作为一个 const 引用。.
  • 有时,我们希望类的数据成员(甚至在 const 成员函数内)可以修改。这可以通过将它们声明为 mutable 来实现。可变数据成员(mutable data member)永远都不能为 const,甚至当它是const 对象的成员时也如此。因此,const 成员函数可以改变 mutable 成员。要将数据成员声明为可变的,必须将关键字 mutable 放在成员声明之前。

构造函数

  • 构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数。构造函数的工作是保证每个对象的数据成员具有合适的初始值。构造函数的名字与类的名字相同,并且不能指定返回类型。像其他任何函数一样,它们可以没有形参,也可以定义多个形参。构造函数可以被重载,实参决定使用哪个构造函数。构造函数自动执行。
  • 构造函数不能声明为 const,创建类类型的 const 对象时,运行一个普通构造函数来初始化该 const 对象。构造函数的工作是初始化对象。不管对象是否为 const,都用一个构造函数来初始化化该对象。
  • 构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。与任意的成员函数一样,构造函数可以定义在类的内部或外部。构造函数初始化只在构造函数的定义中而不是声明中指定。
  • 如果没有为类成员提供初始化式,则编译器会隐式地使用成员类型的默认构造函数。如果那个类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。在这种情况下,为了初始化数据成员,必须提供初始化式。有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。
  • 只要定义一个对象时没有提供初始化式,就使用默认构造函数。为所有形参提供默认实参的构造函数也定义了默认构造函数。一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。内置和复合类型的成员,如指针和数组,只对定义在全局作用域中的对象才初始化。当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。

static类成员

  • 不像普通的数据成员,static 数据成员独立于该类的任意对象而存在;每个 static 数据成员是与类关联的对象,并不与该类的对象相关联。正如类可以定义共享的 static 数据成员一样,类也可以定义 static 成员函数。static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员。
  • 使用static 成员而不是全局对象有三个优点。1. static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。2. 可以实施封装。static 成员可以是私有成员,而全局对象不可以。3. 通过阅读程序容易看出 static 成员是与特定类关联的。这种可见性可清晰地显示程序员的意图。

一个实例

  为了增进读者对上述文字的理解,这里给出一个实例,源自《C++ Primer》习题12.13:扩展Screen类以包含move、set和display操作通过执行如下表达式来测试类:

// 将光标移至指定位置,设置字符并显示屏幕内容 
myScreen.move(4,0).set('#').display(cout);

  答案如下:

 #include <iostream>
#include <string> using namespace std; class Screen {
public:
typedef string::size_type index;
char get() const { return contents[cursor]; }
inline char get(index ht, index wd) const;
index get_cursor() const;
Screen(index hght, index wdth, const string &cntnts); // 增加三个成员函数
Screen& move(index r, index c);
Screen& set(char);
Screen& display(ostream &os); private:
std::string contents;
index cursor;
index height, width;
}; Screen::Screen(index hght, index wdth, const string &cntnts) :
contents(cntnts), cursor(), height(hght), width(wdth) { } char Screen::get(index r, index c) const
{
index row = r * width;
return contents[row + c];
} inline Screen::index Screen::get_cursor() const
{
return cursor;
} // 增加的三个成员函数的定义
Screen& Screen::set(char c)
{
contents[cursor] = c;
return *this;
} Screen& Screen::move(index r, index c)
{
index row = r * width;
cursor = row + c;
return *this;
} Screen& Screen::display(ostream &os)
{
os << contents;
return *this;
} int main()
{
// 根据屏幕的高度、宽度和内容的值来创建
Screen Screen myScreen(, , "aaaaa\naaaaa\naaaaa\naaaaa\naaaaa\n"); // 将光标移至指定位置,设置字符并显示屏幕内容
myScreen.move(, ).set('#').display(cout); return ;
}

  这个解决方法已满足了题目提出的要求,但存在一些缺陷:

  (1) 创建Screen对象时必须给出表示整个屏幕内容的字符串,即使有些位置上没有内容。

  (2) 显示的屏幕内容没有恰当地分行,而是连续显示,因此(4,0)位置上的'#',在实际显示时 不一定正好在屏幕的(4,0)位置,显示效果较差。

  (3) 如果创建的Screen对象是一个const对象,则不能使用display函数进行显示(因为const对 象只能使用const成员)。

  (4) 如果move操作的目的位置超出了屏幕的边界,会出现运行时错误。

  要解决第一个缺陷,可以如下修改构造函数:

 Screen::Screen(index hght, index wdth, const string &cntnts = " "): cursor(), height(hght), width(wdth)
{
// 将整个屏幕内容置为空格
contents.assign(hght*wdth, ' ');
// 用形参string对象的内容设置屏幕的相应字符
if (cntnts.size() != )
  contents.replace(, cntnts.size(), cntnts);
}

  要解决第二个缺陷,可以如下修改display函数:

 Screen& Screen::display(ostream &os)
{
string::size_type index = ;
while (index != contents.size())
{
os << contents[index];
if ((index+) % width == )
{
os << '\n';
}
++index;
}
return *this;
}

  要解决第三个缺陷,可以在Screen类定义体中增加如下函数声明: const Screen& display(ostream &os) const; 声明display函数的一个重载版本,供 const对象使用。
  要解决第四个缺陷,可以如下修改move函数:

 Screen& Screen::move(index r, index c)
{
// 行、列号均从0开始
if (r >= height c >= width)
{
cerr << "invalid row or column" << endl;
throw EXIT_FAILURE;
} index row = r * width;
cursor = row + c;
return *this;
}

  经过如上述几处修改,整个程序的健壮性,鲁棒性都得到了改善。全部代码如下:

 #include <iostream>
#include <string> using namespace std; class Screen {
public:
typedef string::size_type index;
char get() const { return contents[cursor]; }
inline char get(index ht, index wd) const;
index get_cursor() const; Screen(index hght, index wdth, const string &cntnts); Screen& move(index r, index c);
Screen& set(char);
Screen& display(ostream &os);
const Screen& display(ostream &os) const; private:
std::string contents;
index cursor;
index height, width;
}; Screen::Screen(index hght, index wdth, const string &cntnts = ""):
cursor(), height(hght), width(wdth)
{
contents.assign(hght*wdth, ''); if (cntnts.size() != )
contents.replace(, cntnts.size(), cntnts);
} char Screen::get(index r, index c) const
{
index row = r * width;
return contents[row + c];
} inline Screen::index Screen::get_cursor() const
{
return cursor;
} Screen& Screen::set(char c)
{
contents[cursor] = c;
return *this;
} Screen& Screen::move(index r, index c)
{
if (r >= height || c >= width)
{
cerr << "invalid row or column" << endl;
throw EXIT_FAILURE;
} index row = r * width;
cursor = row + c; return *this;
} Screen& Screen::display(ostream &os)
{
string::size_type index = ; while (index != contents.size())
{
os << contents[index];
if ((index + ) % width == )
{
os << '\n';
}
++index;
}
return *this;
} const Screen& Screen::display(ostream &os) const
{
string::size_type index = ; while (index != contents.size())
{
os << contents[index];
if ((index + ) % width == )
{
os << '\n';
}
++index;
}
return *this;
} int main()
{
Screen myScreen(,);
//Screen myScreen(5, 6, "aaaaa\naaaaa\naaaaa\naaaaa\naaaaa\n");
myScreen.move(, ).set('#').display(cout); system("pause"); return ;
}

C++拾遗(五)——类的更多相关文章

  1. javascript函数一共可分为五类: ·常规函数 ·数组函数 ·日期函数 ·数学函数 ·字符串函数

    javascript函数一共可分为五类:    ·常规函数    ·数组函数    ·日期函数    ·数学函数    ·字符串函数    1.常规函数    javascript常规函数包括以下9个 ...

  2. IP分为五类

    IP地址分为五类: IP地址分为五类:A类保留给政府机构,B类分配给中等规模的公司,C类分配给任何需要的人,D类用于组播,E类用于实验. 常用的三类IP地址 IP = 网路地址(网络号)+主机地址(主 ...

  3. 最常用的五类CSS选择器

    一些新手朋友对选择器一知半解,不知道在什么情况下运用什么样的选择器,这是一个比较头疼的问题,针对新手朋友,对CSS选择器作一些简单的说明,希望能对大家的学习工作有一定的帮助,更多的CSS知识请参考We ...

  4. IP分类:A,B,C,D,E五类

    IP地址分为五类: IP地址分为五类:A类保留给政府机构,B类分配给中等规模的公司,C类分配给任何需要的人,D类用于组播,E类用于实验. 常用的三类IP地址 IP = 网路地址(网络号)+主机地址(主 ...

  5. C++面向对象高级编程(五)类与类之间的关系

    技术在于交流.沟通,转载请注明出处并保持作品的完整性. 本节主要介绍一下类与类之间的关系,也就是面向对象编程先介绍两个术语 Object Oriented Programming   OOP面向对象编 ...

  6. 对MySQL性能影响较大的五类配置参数

    以下主要是对MySQL 性能影响关系紧密的五大配置参数的介绍. 一.      连接 连接通常来自Web 服务器,下面列出了一些与连接有关的参数,以及该如何设置它们. (一).             ...

  7. How Javascript works (Javascript工作原理) (十五) 类和继承及 Babel 和 TypeScript 代码转换探秘

    个人总结:读完这篇文章需要15分钟,文章主要讲解了Babel和TypeScript的工作原理,(例如对es6 类的转换,是将原始es6代码转换为es5代码,这些代码中包含着类似于 _classCall ...

  8. 表达式树练习实践:C# 五类运算符的表达式树表达

    目录 表达式树练习实践:C# 运算符 一,算术运算符 + 与 Add() - 与 Subtract() 乘除.取模 自增自减 二,关系运算符 ==.!=.>.<.>=.<= 三 ...

  9. Scala学习五——类

    一.本章要点 类中的字段自动带有getter方法和setter方法 你可以用定制的getter/setter方法替换掉字段的定义,而不必修改使用类的客户端——这就是所谓的”统一访问原则“ 用@Bean ...

  10. Unity 游戏框架搭建 2019 (二十五) 类的第一个作用 与 Obselete 属性

    在上一篇我们整理到了第七个示例,我们今天再接着往下整理.我们来看第八个示例: #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; ...

随机推荐

  1. Behave + Selenium(Python)一:

    Behave 介绍:(来自T先生) 最近一个项目用了behave来做测试,因为之前没有接触过,所以写下最近的心得总结. 做自动化的人估计对selenium已经不是很陌生了,但是对于Behave工具,估 ...

  2. JS---分解质因数

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

  3. msfvenom 摄像头

    4.13 莫名其妙的心情不好 又回到了那个不想打游戏不想聊天不想说话的日子. 不用想.vm——>kali 很早以前看过用msfvenom生成木马的文章.然后……然后我的浏览器就崩溃了.Firef ...

  4. AD 学习

    http://blog.csdn.net/lingpaoershiyishiji/article/details/9139527

  5. xgene:之ROC曲线、ctDNA、small-RNA seq、甲基化seq、单细胞DNA, mRNA

    灵敏度高 == 假阴性率低,即漏检率低,即有病人却没有发现出来的概率低. 用于判断:有一部分人患有一种疾病,某种检验方法可以在人群中检出多少个病人来. 特异性高 == 假阳性率低,即错把健康判定为病人 ...

  6. 利用外部协议让chrome启动外部应用程序

    http://bbs.kafan.cn/thread-1254526-1-1.html 原理:很简单,标题写的很明确了,不懂的google去. 步骤:举个例子,我要启动D:\Programe file ...

  7. 洛谷P2875 [USACO07FEB]牛的词汇The Cow Lexicon

    P2875 [USACO07FEB]牛的词汇The Cow Lexicon 题目描述 Few know that the cows have their own dictionary with W ( ...

  8. Mac终端操作移动硬盘文件命令

    桌面上看到的硬盘都挂载在 /Volumes目录下 例如移动硬盘名为ZTB,有System目录,则应输入命令: cd /Volumes/ZTB/System 注意:进入Volumes目录命令为cd /V ...

  9. Jmeter 跨线程组传递参数 之两种方法

    终于搞定了Jmeter跨线程组之间传递参数,这样就不用每次发送请求B之前,都需要同时发送一下登录接口(因为同一个线程组下的请求是同时发送的),只需要发送一次登录请求,请求B直接用登录请求的参数即可,直 ...

  10. 解决IE6 IE7绝对定位弹层被后面的元素遮住

    解决IE6 IE7绝对定位弹层被后面的元素遮住? 弹层边框一直被后面的元素边框遮住,试了n种方法,只有这个比较好用. <div class="tuijian-table"&g ...