程序员肖邦的博客 享受编程和技术所带来的快乐

C++的类和对象(封装性)

2015-04-22
肖邦
C++

C++的类和对象(封装性)

类和对象(封装性)

一、面向对象

  • 为什么要面向对象 WHY
    • 相比于分而治之的结构化程序设计,强调大处着眼的面向对象程序设计思想,更适合于开发大型软件
    • 得益于数据抽象、代码复用等面向对象的固有特征,软件开发的效率获得极大地提升,成本即大幅降低
    • 面向对象技术在数据库、网络通信、图形界面等领域的广泛应用,已催生出各种设计模式和应用框架
    • 面向对象技术的表现如此出众,以至于那些原本并不直接支持面向对象特性的语言(例如 C),也在越来越多地通过各种方法模拟一些面向对象的软件结构
  • 什么是面向对象 WHAT
    • 万物皆对象,这是人类面对世界最朴素,最自然的感觉、想法和观点
    • 把大型软件看成是一个由对象组成的社会
    • 对象拥有足够的智能,能够理解来自其它对象的信息,并以适当的行为作出反应
    • 对象能够从高局对象继承属性和行为,并允许低层对象从自己继承属性和行为等
    • 编写程序的过程就是描述对象属性和行为的过程,凭借返种能力使问题域和解域获得最大程度的统一。
    • 面向对象的三大要件:封装、继承和多态。
  • 怎样面向对象 HOW
    • 至少掌握一种面向对象的程序设计语言,如 C++
    • 深入理解封装、继承和多态等面向对象的重要概念
    • 精通一种元语言,如 UML,在概念层次上描述设计
    • 学习设计模式,源自多年成功经验的积累和总结

二、类和对象

  • 拥有相同属性和行为的对象被分成一组,即一个类
  • 类可用于表达那些不能直接与内置类型建立自然映射关系的逻辑抽象
  • 类是一种用户自定义的复合数据类型,即包括表达属性的成员变量,也包括表达行为的成员函数
  • 类是现实世界的抽象,对象是类在虚拟世界的实例

三、类的定义与实例化

类的一般形式:

class 类名: 继承方式 基类, ... {                   /* 继承方式 */
    访问控制限定符:
    类名(形参表):成员变量(初值), ... {             /* 构造函数 */
        函数体;                                  /* 初始化表 */
    }
    ~类名(void){函数体;}                          /* 析构函数 */
    返回类型 函数名(形参表) 常属性 异常说明 {函数体;}  /* 成员函数 */
    数据类型 变量名;                               /* 成员变量 */
};

注:在类内定义的成员函数默认是内联函数;若成员函数的声明和实现分开,如果需要手动加 inline

类的控制限定符

public    公有成员 ———— 谁都可以访问
protected 保护成员 ———— 只有自己和子类可以访问
private   私有成员 ———— 只有自己可以访问

访问控制限定符

  • 在 C++ 中,class 和 struct 没有本质性差别,唯一不同在于:
    • class 的缺省访问控制属性为私有(private)
    • struct 的缺省访问控制属性为公有(public)
  • 访问控制限定符仅作用于类,而非作用于对象,因此同一个类的不同对象,可以互相访问非公有部分
  • 对不同成员的访问控制属性加以区分,体现了 C++ 作为面向对象程序设计语言的封装特性

构造函数

  • 函数名与类名相同,且没有返回值
  • 在创建对象时自动被调用,且仅被调用一次,主要目的是初始化对象
    • 对象定义语句
    • new 操作符
  • 为成员变量赋初值,分配资源,设置对象的初始状态
  • 对象的创建过程
    • 为整个对象分配内存空间
    • 以构造实参调用构造函数
      • 构造基类部分
      • 构造成员变量
      • 执行构造代码

类的声明和实现可以分开

class 类名 {
    返回类型 函数名 (形参表);
};

返回类型 类名::函数名 (形参表) {
    函数体;
};

栈中对象的创建与销毁

  • 在栈中创建单个对象
    • 类名 对象; /* 注意不要加() */
    • 类名 对象(实参表);
  • 在栈中创建对象数组
    • 类名 对象数组[n];
    • 类名 对象数组[n] = {类名 (实参表),… …};
    • 类名 对象数组[] = {类名 (实参表),… …};

堆中对象的创建与销毁

  • 在堆中创建/销毁单个对象
    • 类名* 对象指针 = new 类名;
    • 类名* 对象指针 = new 类名 ();
    • 类名* 对象指针 = new 类名 (实参表);
    • delete 对象指针;
  • 在堆中创建/销毁对象数组
    • 类名* 对象数组指针 = new 类名[元素个数];
    • 类名* 对象数组指针 = new 类名[元素个数] {类名 (实参表), …};
    • // 上面的写法需要编译器支持 C++11 标准
    • delete[] 对象数组指针;

知识点

  • 在类的成员函数的声明与实现分开写时,要注意在实现部分一定要在函数名前加 类名::
  • 在类的定义时,注意大括号{}后边一定要加分号 ‘;’
  • 如果类内的成员函数设为访问权限 private,而且在类内是没有被调用,则在代码区是没有映射区域的在汇编代码中是找不到它的对应名字的。
  • 如果类内的成员函数设为访问权限 public,则是在代码区映射内存的,可以使用汇编代码中对应的函数名调用该成员函数,效果一样

四、构造函数与初始化表

构造函数可以重载

  • 构造函数也可以通过参数表的差别形成重载
  • 重载的构造函数通过构造实参的类型选择匹配
  • 不同的构造函数版本表示不同的对象的创建方式
  • 使用缺省参数可以减少构造函数重载的版本数量
  • 某些构造函数具有特殊的意义
    • 缺省构造函数:按缺省方式构造
    • 类型转换构造函数:从不同类型的对象构造
    • 拷贝构造函数:从相同类型的对象构造

缺省构造函数

  • 缺省构造函数也称为无参构造函数,未必真的没有任何参数,为一个有参构造函数的每个参数都提供一个缺省值,同样可以达到无参构造函数的效果
  • 如果一个类没有定义任何构造函数,那么编译器会为其提供一个缺省构造函数
    • 对基本类型的成员变量,不做初始化
    • 对类类型的成员变量和基类子对象,调用相应类型的缺省构造函数初始化
    • 对于 int 等基本类型变量,什么都不干
    • 对于 string 类型,调用相应的缺省构造函数,其缺省构造函数对 string 类型的成员变量初始化为空串
  • 如果构造对象能够以无参的形式调用该构造函数,那么该构造函数就是缺省构造函数
  • 对于已经定义至少一个构造函数的类,无论其构造函数是否带有参数,编译器都不会再为其提供缺省构造函数

缺省构造函数的特殊性

  • 有时必须自己定义缺省构造函数,即使什么也不做,尤其是在使用数组和容器的时候。
  • 有时必须为一个类提供缺省构造函数,仅仅因为它可能作为另一个类的子对象而被缺省构造
  • 若子对象不宜缺省构造,则需要为父对象提供缺省构造对象,并为其中显式地以非缺省方式构造该子对象

类型转换构造函数

  • 在目标类型中,可以接受单个源类型对象实参构造函数,支持从源类型到目标类型的隐式类型转换
    class 目标类型 {
      目标类型(const 源类型& src) { ... }
    };
    
  • 通过 explicit 关键字,可以强制这种通过构造函数实现的类型转换必须显式进行
    class 目标类型 {
      explicit 目标类型(const 源类型& src) {...}
    };
    
  • 示例
    class Dog;     /* 短式声明,声明Dog是class */
    class Cat{
    public:
      explicit Cat (const Dog& dog); /* 转换构造函数中 explicit 显式转换构造 */
    };
    class Dog {
    private:
      string m_name;
      friend class Cat;  /* 将猫设为狗的友元,猫就可以访问狗的私有成员 */
    };
    Cat::Cat (const Dog& dog) {
      m_name = dog.m_name;
    }  /* 转换构造函数的实现部分 */
    main():
    Dog dog;   Cat cat(dog); /* 等同于 */
    Cat cat = dog;           /* 都是初始化, 2种写法 */
    Cat cat2;  cat2 = dog;   /* 赋值也可以的 */
    Cat cat2;  cat2 = static_cast<Cat>(dog);
    /* 当用expicit显式声明转换构造函数时,就要用static_cast进行转换 */
    
  • 示例2:思考
    string str; str = "Hello World"; // 赋值
    string str = "Hello World";      // 初始化
    // 为何"Hello World"字面值是 const char* 的类型,str 是 string 类型,却可以赋值 ?
    // 答:首先创建 str 对象,在创建对象时调用了一个构造函数,这个构造函数就是类型转换构造函数
    // string(const char *p) { ... }
    

拷贝构造函数

  • 定义方式
    • class 类名 { 类名(const 类名& that) {...} };
    • 拷贝构造函数,用于从一个已经定义的对象构造其同类型的副本,即克隆
  • 如果一个类没有定义拷贝构造函数,那么编译器会为其提供一个缺省拷贝构造函数
    • 对基本类型成员,按字节复制
    • 对类类型成员变量和基类子对象,调用相应类型的拷贝构造函数
  • 如果自己定义了拷贝构造函数,编译器将不再提供缺省拷贝构造函数,这时所有与成员复制有关的操作,都必须在自定义拷贝构造函数中编写代码完成
  • 若缺省拷贝构造函数不能满足要求,则需要自己定义
  • 拷贝构造的时机
    • 用已定义对象作为同类型对象的构造实参
    • 以对象的形式向函数传递参数
    • 从函数中返回对象
    • 某些拷贝构造过程会因编译优化而被省略
  • 拷贝构造函数有风险,需要复制时效率降低,而且有可能引发错误,编译器是能不做就不做拷贝构造函数,进行优化

自定义构造函数和系统定义构造函数

  • 无自定义构造函数,那么系统定义的有:缺省构造函数、缺省拷贝构造函数
  • 自定义除拷贝构造函数以外的任何构造函数,那么系统定义:缺省拷贝构造函数
  • 自定义拷贝构造函数,那么系统定义:无

初始化表

  • 通过在类的构造函数中使用初始化表,指明类的成员变量如何被初始化
  • 数组和结构型成员变量需要用花括号 {} 初始化
  • 类的类类型成员变量和基类子对象,必须在初始化表中显式初始化,否则将调用相应类的缺省构造函数的初始化。而且不能在构造函数体内初始化,因为在构造函数体内属于赋值操作,而要赋值则必须先定义构造对象,矛盾。
  • 类的常量和引用型成员变量,必须在初始化表中显式初始化
  • 类的成员变量按其在类中的声明顺序依次被初始化,而与其在初始化表中的顺序无关
    Time(int hour, int min, int sec): m_hour(hour), m_min(min), m_sec(sec) {}
    Student(const string& name, int age): m_name(name),m_age(age),m_time(12,45,56) {}
    
  • 初始化表与构造函数体内的初始化方式的一种差别?
    • 初始化表的初始化方式理论上比构造函数方式在性能方面好。
    • 因为在构造函数体内的”初始化”,其实是在定义变量时以不确定值进行初始化,然后再通过构造函数体内给其进行赋值操作
    • 在我们看来对象在构造的时候就是初始化,显然性能不如初始化表
    • 对于基本类型的成员变量来讲区别不大,但是对于复杂的类型就不一样了
  • 成员变量是数组或结构体。
    • 初始化时需要在初始化表中用 A(): m_arr{10,20,30} {}
    • 在构造函数体内初始化数组:m_arr = {10,20,30} ,不要加 []
    • 编译器需要支持 C++11 标准,编译时需要 -std=c++0x-std=gnu++0x
  • 尽量避免成员变量之间的相互依赖

五、this指针

C++对象模型

  • 相同类型的不同对象各自拥有独立的成员变量实例。
  • 相同类型的不同对象彼此共享同一份成员函数代码
  • 在代码区中,为相同类型的不同对象所共享的成员函数,如何区分所访问的成员变量隶属于哪个对象?
  • 类的每个成员函数、构造函数和析构函数,都有一个隐藏的指针参数 this ,指向调用该成员函数、正在被构造或正在被析构的对象,这就是 this 指针。
  • 在类的成员函数、构造函数和析构函数中,对所有成员的访问,都是通过 this 指针进行。与 C 没有本质区别,提供同样的二进制模型,但是可以提供面向对象的机制。
  • 从编译器层面理解 this 指针:
    void print(User *this) {
      cout << this->m_name << endl;
    }
    user1.print();     // print(&user1)
    nm a.out           // 08048bee W _ZN4User5printEv -> E 就是 this 指针
    

this指针的应用

  • 多数情况下,程序并不需要显式地使用 this 指针。
  • 有时候为了方便,将一个类的某个成员变量与该类的构造函数的相应参数取相同标示符,这时在构造函数内部可通过 this 指针将二者加以区分,有时候不方便修改名字。
  • 返回基于 this 指针的自引用,以支持串联调用
    Counter& inc(void) {
      ++m_i;
      return *this
    }
    Counter c; c.inc().inc().inc();
    a + b + c     // 多用于操作符重载的情况下,常用情况之一
    
  • this 指针作为函数的参数,以实现对象交互

六、常函数与常对象

  • 在类成员函数的形参表之后,函数体之前加上 const 关键字,该成员函数的 this 指针即具有常属性,这样的成员函数被称为常函数,修饰的是 this 指针,this 指向目标不能改。
    • 返回类型 函数名(形参列表) const { ... }
  • 在常函数内部无法修改成员变量的值,除非该成员变量被 mutable 关键字修饰
  • 被 const 关键字修饰的对象、对象指针或对象引用,统称为常对象
    • const User user (...);
    • const User* ptr = &user;
    • const User& ref = user;
  • 通过常对象只能调用常函数,通过非常对象既可以调用常函数,也可以调用非常函数
  • 原型相同的成员函数,常版本与非常版本构成重载 (this 指针的存在)
    • 常对象只能选择常版本
    • 非常对象优先选择非常函数版本,如果没有非常函数版本,也能选择常函数版本
  • 函数右大括号 “}” 的作用
    • 大括号是一条语句,被汇编以后展开若干条指令,其中有一条是用来弹函数栈的,在弹栈之前要调用这里所有的局部对象的析构函数
    • 在堆里分配对象空间是由 delete 调用析构函数
    • 在栈中是由右大括号 “}” 调用析构函数

七、析构函数

  • 析构函数的函数名就是在类名前加 ~,没有返回类型也没有参数,不能重载
  • 在销毁对象时自动被调用,且仅被调用一次
    • 对象离开作用域
    • delete 操作符
  • 释放在对象的构造过程或声明周期内所获得的资源
  • 其功能并不局限于在释放资源上,它可以执行任何类设计者希望在最后一次使用对象之后执行的动作
  • 通常情况下,若对象在其生命周期最终时刻,并不持有任何动态分配的资源,可以不定义析构函数
  • 如果一个类没有定义析构函数,那么编译器会为其提供一个缺省析构函数
    • 对于基本类型的成员变量,什么也不做 对类类型的成员变量和基类子对象,调用相应类型的析构函数
  • 对象的销毁过程 (与构造函数顺序正好完全相反)
    • 调用析构函数(执行析构代码、析构成员变量、析构基本部分)
    • 释放整个对象所占用的空间

八、拷贝构造与拷贝赋值

  • 缺省方式的拷贝构造和拷贝赋值,对包括指针在内的基本类型成员变量按字节复制,导致 浅拷贝 问题
  • 为了获得完整意义上的对象副本,必须自己定义拷贝构造和拷贝赋值,针对指针型成员变量做 深拷贝
  • 相对于拷贝构造,拷贝赋值需要做更多的工作
    • 避免自赋值
    • 分配新资源
    • 拷贝新内容
    • 释放旧资源
    • 返回自引用
  • 尽量复用拷贝构造函数和析构函数中的代码
    • 拷贝构造:分配新资源、拷贝新内容
    • 析构函数:释放旧资源
  • 无论是拷贝构造还是拷贝赋值,其缺省实现对类类型成员变量和基类子对象,都会调用相应类型的拷贝构造函数和考别赋值运算符函数,而不是简单地按字节复制,因此应尽量避免使用指针型成员变量。
  • 尽量通过引用或指针向函数传递对象型参数,既可以降低参数传递的开销,也能减少拷贝构造的机会
  • 处于具体原因的考虑,确实无法实现完整意义上的拷贝构造和拷贝赋值,可将它们私有化,以防误用
  • 如果为一个类提供了自定义的拷贝构造函数,就没有理由不提供实现相同逻辑的拷贝赋值运算符函数

九、静态成员

  • 静态成员属于类而不属于对象。
    • 静态成员变量不包含在对象实例汇总,进程级生命期
    • 静态成员函数没有 this 指针,也没有常属性 (const 就是用来修饰 this 指针的)
    • 静态成员依然受 类作用域访问控制限定符 的约束
  • 静态成员变量的定义和初始化,只能在类的外部而不能在构造函数中进行
  • 静态成员变量为该类的所有对象实例所共享(在内存中多个对象只有一份)
  • 访问静态成员,既可以通过类也可以通过对象
  • 静态成员函数只能访问静态成员,而非静态成员函数既可以访问静态成员,也可以访问非静态成员,但是可以访问构造函数,因为构造函数不依赖于对象(构造函数是创建对象的)
  • 事实上,类的静态成员变量和静态成员函数,更像是普通的全局变量和全局函数,只是多了一层类作用域和访问控制属性的限制,相当于具有了成员访问性的全局变量和全局函数。

十、单例模式

  • 一个类只有一个实例,通过全局访问获取
  • 将包括拷贝构造函数在内的所有构造函数私有化,防止类的使用者从类的外部创建对象
  • 公有静态成员函数 getInstance() 是获取对象实例的唯一渠道
  • 饿汉式:无论用不用,程序启动即创建
  • 懒汉式:用的时候创建,不用了即销毁
    • 永不销毁
    • 引用计数
    • 线程安全

十一、成员指针

  • 成员变量指针
    • 类型 类名::*成员变量指针;
    • 成员变量指针 = &类名::成员变量
    • 对象.成员变量指针 或 对象指针->成员变量指针
    • 本质就是特定成员变量在对象实例中的相对地址,解引用时再根据调用对象的地址计算出该成员变量的绝对地址
  • 成员函数指针
    • 返回类型 (类名::*成员函数指针)(形参表)
    • 成员函数指针 = &类名::成员函数名;
    • (对象.成员函数指针)(实参表) 或 (对象->成员函数指针)(实参表)
    • 虽然成员函数并不存储在对象中,但也要通过对象或对象指针对成员函数指针解引用,其目的只有一个,即提供 this 指针
  • 静态成员变量指针
    • 类型 *静态成员变量指针;
    • 静态成员变量指针 = &类名::静态成员变量
    • *静态成员变量指针
  • 静态成员函数指针
    • 返回类型 (*静态成员函数指针)(形参表)
    • 静态成员函数指针 = 类名::静态成员函数名
    • 静态成员函数指针(实参表)
  • 静态成员与对象无关,因此静态成员指针与普通指针并没有本质区别

Comments

Content