第四章 1面向对象

1、类和对象

  • 对象:现实中对象的模拟,具有属性(数据成员)和行为(函数成员),对象是类的实例
  • 类:同一类对象的共同属性和行为
  • 定义对象时,通过构造函数初始化
  • 删除对象时,通过析构函数释放资源

2、面向对象程序设计的基本特点:抽象、封装、继承和多台

抽象

  • 对同一类对象的共同属性和行为进行概况,形成类。
    • 首先注意问题的本质及描述,其次是实现过程或细节
    • 数据抽象:描述某类对象的类型和状态
    • 代码抽象:描述某类对象的共有的行为特征或具有的功能
    • 抽象的实现:类

抽象实例


image.png

image.png

封装

  • 将抽象出来的数据成员、代码成员相结合,将他们视为一个整体
    • 目的:增强安全性和简化编程,使用者不必了解具体的实现细节,只需要通过外部的接口,以特定的访问权限,来使用类的成员。
    • 实现封装:类声明中的{}
      image.png

      public:类的对外访问接口
      private:在类的外面是看不到的,只有类的对象可以访问

继承

  • 在已有类的基础上,进行扩展形成新的类

多态

  • 多态:同一名称,不同功能实现方式
  • 目的:达到行为标识统一,减少程序中标识符的个数

3、 类和对象的定义

image.png

定义类的语法形式

class 类名称
{
  public:
              公有成员(外部接口)
  private:
              私有成员
  public:
              保护性成员
}

类内初始值

  • 当构造函数没有对数据成员进行初始化,那么就会用类内初始值进行初始化
  • 为数据成员设置类内初始值
  • 用于初始化函数成员


    image.png

类成员的访问控制

  • 公有类型成员
  • 私有类型成员
  • 保护类型成员


    image.png

    image.png

    image.png

4、定义类的对象

  • 语法
    类名 对象名;
  • 类中的成员之间直接使用,成员名互相访问
  • 类外访问类的成员使用 “对象名.成员名” 方式访问pulic成员
  • 类外不能访问类的private成员,类内随便访问

5、类的成员函数

成员函数的实现

  • 语法
    返回值类型 类名::函数名(){}

成员函数

  • 在类中声明函数原型
  • 可以在类外给出函数体的实现,并在函数名前使用类名加以限定
  • 也可以直接在类中给出函数体,形成内联成员函数
  • 允许声明重载函数和带默认参数值的函数(一般是构造函数)
  • 类的成员函数不占用空间,只有数据成员才会占用空间,一般我们说的空间指的是存储空间,内存分成存储空间和代码空间,而成员函数放在了代码空间

内联成员函数

  • 为了提高运行时的效率,对较简单的函数可以声明为内联形式
  • 内联函数体中不要有复杂结构(如循环语句和switch语句)
  • 在类中声明内联成员函数的方式
    • 将函数体放在类的声明中(构造函数通常写成内联函数的形式)
    • 在类外 使用inline ,格式为:inline 返回值类型 函数名(形参){}

6、构造函数

  • 类中特殊函数
  • 用于描述初始化算法

构造函数的作用

在对象被创建时使用特定的值构造对象

例如:
希望在构造一个Clock类对象时,
将初始时间设置为0:0:0,
就可以通过构造函数来设置。

构造函数的形式

  • 函数名与类名相同
  • 不能定义返回值类型,也不能有return语句
  • 可以有形式参数,也可以没有形式参数
  • 可以是内联函数
  • 可以重载
  • 可以带默认参数值(调用时,如果给出实参那么用实参的值,如果没有实参,那么就用默认参数值)

构造函数的调用时机

  • 在对象创建时被自动调用
  • 例如:
    Clock myClock(0,0,0)

默认构造函数

  • 调用时可以不需要实参的构造函数
    • 参数表为空的构造函数
    • 全部参数都有默认值的构造函数
      下面两个全都是默认构造函数,但是不能在类中同时出现(因为在调用时,都不给实参,编译器无法判断调用哪一个)
//声明
Clock();//没有形参,参数表为空
Clock(int newH=0,int newM=0,int newS=0);//有形参,但是形参都是默认值,所以实参可以省略
//定义
Clock::Clock( )
{
   一些初始化操作
    cout << "Object is being created" << endl;
}

调用上面两个构造函数,都可以不给出实参。

隐含生成的构造函数

在建立新的对象时,一定会调用构造函数,不声明任何构造函数的话,编译器会自动帮我们生成一个默认构造函数
隐含生成的构造函数:

  • 参数列表为空,不能为数据成员设置初始值
  • 如果类内定义了成员的初始值,则使用内类定义的初始值
  • 如果没有定义类内的初始值,则以默认方式初始化
  • 基本类型的数据默认初始化的值是不确定的
    总的来说就是隐含生成的构造函数,不会为数据成员初始化,如果成员在类内初始化了,那么就用那么值,如果没有,就用默认的方式初始化(初始化的值是不确定的)

#default

如果程序中已定义构造函数,默认情况下编译器就不再隐含生成默认构造函数,但此时如果仍希望系统生成默认生成的构造函数就可以使用"=default",这样就有两个构造函数可以使用了,用户自己生成的构造函数仍然可以使用

// use of defaulted functions
#include <iostream>
using namespace std;

class A {
public:
    //  user-defined
    A(int x){
        cout << "This is a parameterized constructor";
    }
    //Using the default specifier to instruct
    // the compiler to create the default implementation of the constructor.
    A() = default;
};

int main(){
    A a;          //call A()
    A x(1);       //call A(int x)
    cout<<endl;
    return 0;
} 

带参数的构造函数

默认的构造函数(即使有形参,形参也都有默认值)和隐含生成的构造函数都没有参数,在创建对象时不需要任何参数,但是如果需要构造函数也可以带参数,这样在创建对象时就会给对象赋初始值,例子如下

class Line
{
   public:
     ...
      Line(double len);  // 这是构造函数
   private:
      double length;
};
// 构造函数定义
Line::Line( double len)
{
    cout << "Object is being created, length = " << len << endl;
    length = len;
}
// 程序的主函数
int main( )
{
   Line line(10.0);
   return 0;
}

类中可以同时存在两个构造函数,一个默认构造函数(没有形参)由用户或者系统生成;一个带参数的构造函数。当用户自己定义了构造函数,系统将不会再隐含生成的默认构造函数,不过可以用 =default让系统生成


7、在程序中定义和使用构造函数

初始化列表

  • 与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段
  • 初始化列表中赋值不能用等号A=B 而是要用括号A(B)
  • 从概念上来讲,构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段

例1 带有参数的构造函数

class Clock{
    public:
    Clock(int newH,int newM,int newS);//带有参数的构造函数
    void setTime(int newH,int newM,int newS);
    void showTime();
    private:
    int hour,minute,second;
}
//构造函数的实现
Clock::Clock(int newH,int newM,int newS):
hour(newH),minute(newM),second(newS){}
// : 初始化列表 {   
//如果对类的对象进行简单初始化,首选这种方式
//等价于hour=newH;   minute=newM; second=newS;
...
int main(){
    Clock c(0,0,0);//自动调用构造函数
}

例 2带有参数的构造函数和默认构造函数

class Clock{
    public:
    Clock(int newH,int newM,int newS);//带有参数的构造函数
    Clock();//构造函数的重载
    void setTime(int newH,int newM,int newS);
    void showTime();
    private:
    int hour,minute,second;
}
//构造函数的实现
Clock::Clock(int newH,int newM,int newS):
hour(newH),minute(newM),second(newS){}
//默认构造函数的实现
Clock::Clock():hour(0),minute(0),second(0){}
//将hour minute second的值都设置为0
/*   : 初始化列表 {   
如果对类的对象进行简单初始化,首选这种方式
hour=newH
minute=newM
second=newS
*/
...
int main(){
    //创建对象时后面有参数,调用有参数的构造函数,没参数时,调用无参数/默认构造函数
    Clock c1(8,10,0);//自动调用构造函数  8,10,0
    Clock c2;//自动调用构造函数  0,0,0
}

8、委托构造函数

什么是委托构造函数?
一个构造函数可以委托另一个构造函数帮它完成初始化功能
委托构造函数好处:

  • 不需要反复写构造函数
  • 保持代码实现的一致性,修改构造函数时,连同委托它实现的构造函数一并修改
    例子:
//构造函数的实现
Clock::Clock(int newH,int newM,int newS):
hour(newH),minute(newM),second(newS){}
//默认构造函数的实现
Clock::Clock():hour(0),minute(0),second(0){}

原本的构造函数可以用委托构造函数来实现

//构造函数的实现
Clock::Clock(int newH,int newM,int newS):
hour(newH),minute(newM),second(newS){}
//用委托构造函数实现默认构造函数
Clock::Clock():Clock(0,0,0){}

9、复制构造函数

  • 定义:复制构造函数是一种特殊的构造函数,其形参为本类的对象引用。
  • 作用:是用一个已存在的对象去初始化同类型的新对象。

复制构造函数的声明以及实现

class 类名{
    public:
    类名(形参);//构造函数
    类名(const 类名 &对象名);//复制构造函数,
    //在引用前面加const是为了防止引用的双向传递造成一些错误,
    //所以加const使引用变成了单向传递
    ...
};
//定义
类名::类名(const 类名 &对象名)//复制构造函数的实现
{
    //函数体
}

复制构造函数被调用的三种情况(什么时候会用到复制构造函数)

  • 定义一个对象时,以本类的另一个对象作为初始值,发生复制构造;(赋值)
  • 如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象,发生复制构造(作形参)
  • 如果函数的返回值是类的对象,函数执行完成后返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造。(作返回值)

隐含的复制构造函数

  • 如果程序员没有为类声明拷贝初始化(复制)构造函数,则编译器自己生成一个隐含的复制构造函数
  • 这个隐含的复制构造函数的功能是:用初始值对象每个数据成员,初始化将要建立的对象的对应数据成员
  • 一般情况下,编译器自己生成的隐含构造函数就够用,如果类的成员中有指针,很多情况下默认的隐含构造函数实现的浅层的复制构造就不够用了,需要做深层的复制构造
  • 当需要做特定功能的复制构造(只复制一部分/修改一些),也需要自己实现复制构造函数

#delete

如果不希望对象被复制构造
C++98做法:将复制构造函数声明为private,且不提供函数的实现
C++11做法:用“#delete”指示编译器不生成默认复制构造函数


image.png
using namespace std;
class Point
{
public:
    Point(int xx = 0, int yy = 0) { x = xx; y = yy; };
    Point(Point& p);
    int getX() { return x; };
    int getY() { return y; };
private:
    int x, y;
};
Point::Point(Point& p)
{
    x = p.x;
    y = p.y;
    cout << "calling the copy constructor" << endl;
}
void fun1(Point p)
{
    cout << p.getX() << endl;
}
Point fun2()
{
    Point a;
    return a;
}
int main()
{
    Point a;
    Point b(a);
    cout << b.getX() << endl;
    fun1(b);
    b = fun2();
    cout << b.getX() << endl;
    return 0;
}

10 、析构函数

  • 析构函数的作用:析构函数完成对象被删除前的一些清理工作(例如删除new开辟的空间)
  • 调用时机:在对象的生存期结束的时刻,系统自动调用析构函数
  • 默认析构函数:如果程序中未声明析构函数,编译器将自动产生一个默认的析构函数,其函数体为空(可以理解成什么都不做的函数)
  • 析构函数的原型: ~类名();
  • 析构函数没有参数,没有返回类型,也不允许有return语句

例子:


image.png

11、类的组合

类的组合:一些类的对象可以作为另外类的部件
组合的概念

  • 类中的成员是另一个类的对象
  • 可以在已有抽象的基础上实现更复杂的抽象。

构造函数如何实现?

组合类没有权利访问部件对象的内部私有成员,让部件对象调用自己的构造函数

实现:

组合类的构造函数负责将部件对象初始化所需要的参数传递给他,编译器会自动调用部件类的构造函数初始化这些部件对象

类组合的构造函数设计

  • 原则:不仅要负责对本类中的基本类型成员初始化,也要对对象成员初始化
  • 类组合构造函数声明及定义形式:

类名:类名(对象成员所需要的形参,本类成员形参):对象1(参数),对象2(参数),....{
//函数体其他语句
}
例如

//Line类中成员初始化都不需要参数
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
    cout << "Calling constructor of Line" << endl;
    double x = static_cast<double>(p1.getX() - p2.getX());
    double y = static_cast<double>(p1.getY() - p2.getY());
    len = sqrt(x * x + y * y);
}

构造组合类对象时的初始化次序

  • 先部件对象初始化构造,然后再是本来进行构造。

  • 在部件对象初始化中,初始化顺序是根据部件对象在类中声明的顺序,而不是部件对象在列表中初始化的顺序。


  • 在组合类的构造中有特殊的情况,比如有些部件对象没有在构造函数的初始化列表中,那么该部件对象的默认构造函数将会被执行。记住这是默认构造函数,如果我们自己重新定义了默认构造函数,那么编译器生成的默认构造函数就不存在了,而调用我们自己定义的默认构造函数,并且这个构造函数不能带参数(有参数也必须都是默认值)

  • 需要注意的是,有些数据成员的初始化必须要构造函数的初始化列表中进行。一类是没有默认构造函数的部件对象,因为这类对象初始化时必须提供参数(为什么必须提供参数,因为这类对象只有带参数的构造函数,没有默认构造函数,而编译器也不会为他生成构造函数,如果这类对象不在初始化列表中,就会报错,无法初始化),另一类是引用类型的数据成员——因为引用型变量必须在初始化时绑定引用的对象。

组合类的代码实例

#include <iostream>
#include <cmath>
using namespace std;
class Point {
public:
    Point(int xx = 0, int yy = 0) {
        x = xx;
        y = yy;
    }
    Point(Point& p);
    int getX() { return x; }
    int getY() { return y; }
private:
    int x, y;
};
Point::Point(Point& p) {
    x = p.x;
    y = p.y;
    cout << "Calling the copy constructor of Point" << endl;
}
class Line {
public:
    Line(Point xp1, Point xp2);
    Line(Line& l);
    double getLen() { return len; }
private:
    Point p1, p2;
    double len;
};
//利用了对象p1和p2的复制函数对p1进行初始化
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
    cout << "Calling constructor of Line" << endl;
    double x = static_cast<double>(p1.getX() - p2.getX());
    double y = static_cast<double>(p1.getY() - p2.getY());
    len = sqrt(x * x + y * y);
}
//复制构造函数,将对象l的l.p1赋给当前对象的p1,l.p2赋给当前对象的p2
Line::Line(Line& l) : p1(l.p1), p2(l.p2) {
    cout << "Calling the copy constructor of Line" << endl;
    len = l.len;
}
int main() {
    Point myp1(1, 1), myp2(4, 5);//建立Point类的对象
    Line line(myp1, myp2);//建立Line类的对象,形实结合传参数的时候,先从后面开始传
    Line line2(line);//利用复制构造函数建立一个新的对象,用已经构造好的line初始化line2
    cout << "The length of the line is:";
    cout << line.getLen() << endl;
    cout << "The length of the line2 is:";
    cout << line2.getLen() << endl;
    return 0;
}

12、前向引用声明

两个类互相引用,那么该如何声明? 使用前向引用声明

  • 类应该先声明,后使用
  • 如果需要在某个类的声明之前引用该类,则应进行向前引用声明
  • 向前引用声明只为程序引入一个标识符,但具体的声明在其他地方。

两个类互相引用的例子:

class B;//向前引用声明

class A{
    public:
        void f(B b);//A的成员函数f的参数是B类型的对象
};

class B{
    public:
        void g(A a);//B的成员函数f的参数是A类型的对象
}

前向引用声明注意事项

  • 在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类对象。
  • 当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节。
class B;//向前引用声明

class A{
    public:
        B b;//错误,在提供完整类的声明之前,不能声明该类的对象,没有完整声明的都不知道该类具体细节比如说占用的大小,不能进行声明该类的对象
        void f(B b);//正确,这里只是把类B的对象b当做形参,作形参只是使用了该符号,没有占用空间
};

class B{
    public:
        A a;//错误,在提供完整类的声明之前,不能声明该类的对象
        void g(A a);//正确
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352