C++ 面向对象编程:封装

面向对象编程的三大特性:

  • 封装
  • 继承
  • 多态

封装的要点

  • 将属性和行为作为整体管理
  • 对属性和行为进行权限控制

属性和行为

  • 属性:成员属性、成员变量
  • 行为:成员函数、成员方法

访问权限

值得注意的是,访问权限限定的是类,而不是对象。例如在拷贝构造函数中可以调用相同类,不同对象的私有成员

  • public:类内可以访问,类外可以访问,子类中可以访问
  • protected:类内可以访问,子类中可以访问,类外不可以访问
  • private:类内可以访问,子类中不可以访问,类外不可以访问

将属性设为私有的好处

  • 控制读写权限
  • 可以检测数据的有效性

类的分文件编写

  1. 创建头文件(.h)和源文件(.cpp)
  2. 在头文件中编写成员变量和成员函数声明
  3. 在源文件中编写成员函数定义
  4. 在使用这个类时,要先引入包含这个类的头文件

构造函数和析构函数

  • 构造函数:执行成员属性初始化,由编译器自动调用
  • 析构函数:执行清理工作,在对象销毁前自动调用

构造函数语法

ClassName()
{
    // 成员属性初始化
}
  • 构造函数没有返回值,也不写 void
  • 构造函数名与类名相同
  • 构造函数可以有参数,因此可以发生函数重载
  • 程序创建对象时自动调用构造函数,且只调用一次

析构函数语法

~ClassName()
{
    // 执行清理工作
}
  • 析构函数没有返回值,也不写 void
  • 析构函数名为:~类名
  • 析构函数不可以有参数,因此不可以发生函数重载
  • 程序销毁对象时自动调用析构函数,且只调用一次

拷贝构造函数

ClassName(const ClassName& object)

构造函数的调用

// 默认构造
ClassName object;  // 默认构造:注意不要加括号,否则会被当做函数声明

// 括号法
ClassName object(10);  // 括号法:有参构造函数
ClassName object(anotherOBject);  // 括号法:拷贝构造函数

// 显式法
ClassName object = ClassName(10);  // 显式法:有参构造函数
ClassName object = ClassName(anotherOBject);  // 显式法:拷贝构造函数

// 隐式法
ClassName object = 10;  // 隐式法:有参构造函数
ClassName object = anotherOBject;  // 隐式法:拷贝构造函数

匿名对象

ClassName(10);
  • 当前行执行后,系统会回收匿名函数
  • 不要利用拷贝构造函数初始化匿名对象,编译器会理解成多重定义

拷贝函数的调用时机

  • 使用一个创建完的对象初始化新对象
  • 以值传递的方式给函数参数传值
  • 以值的方式返回局部对象

对象中默认存在的函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,构造体为空)
  3. 默认拷贝构造函数(对属性进行浅拷贝)
  4. 赋值运算符(operator=,对属性进行浅拷贝)

注意:

  • 如果用户自定义拷贝构造函数,那么编译器将不再提供其它构造函数
  • 如果用户自定义有参构造函数,那么编译器将不再提供无参构造函数

拷贝构造函数 –> 有参构造函数 –> 无参构造函数 (前面的有了,后面的编译器就不再自动提供了)

深拷贝和浅拷贝

问题:默认的拷贝构造函数为浅拷贝,如果有成员变量开辟在堆区,那么拷贝的就是相同变量的内存地址。如果两个对象都调用析构函数,就会出现重复释放内存的问题。

解决方法:自己提供拷贝构造函数,对堆区数据进行深拷贝

// 成员变量为堆区开辟的数据
int* m_height;

// 拷贝函数中进行深拷贝
m_height = new int(*p.m_height);

// 析构函数中释放内存
delete m_height;

初始化列表

ClassName(int a, int b): m_a(a), m_b(b)
{
   // 构造函数中的其他内容
}

构造函数和析构函数的调用顺序

  • 构造函数:先构造成员对象,在构造自身对象
  • 析构函数:先析构自身对象,在析构成员对象

静态成员变量

  • 所有对象共享一份数据
  • 在编译阶段分配内存
  • 静态成员变量也是有访问权限的
  • 类内声明类外初始化
// 类外初始化
int Person::age = 18;

// 访问方式
Person p;
cout << p.age;  // 通过对象
cout << Person::age;  // 通过类

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量

类的对象模型

  • 只有非静态成员变量才属于类的对象
  • 空对象所占的内存大小为 1,用于识别空对象所占的内存的位置
  • 有非静态成员变量的,大小为非静态成员变量之和
  • 成员函数不属于类的对象

this 指针

this 指针指向被调用的成员函数所属的对象

  • 形参和成员变量同名时,用 this 进行区分
  • 非静态成员函数中返回对象本身:return *this

空指针访问成员函数

Person* p = nullptr;
p->showPersonName();

注意:如果成员函数中用到了 this 指针(例如使用了成员变量),那么程序有可能崩溃。可以在成员函数中增加下列代码以提高稳健性

if (this == nullptr) return;

常函数

常函数不能修改成员属性。在成员函数后加 const,相当于修饰 this 指向的值。在成员函数前加 const,相当于修饰函数的返回值

void showPersonName() const;  // 常函数
const int personAge();  // 修饰函数的返回值

如果要让某个成员变量在常函数中可修改,可以在成员变量前加上 mutable

class Person
{
    mutable int age;
}

常对象

const Person p;
  • 常对象不能修改成员变量,有 mutable 修饰的除外
  • 常对象只能调用常函数,否则可能间接修改成员变量

友元

利用 friend 关键字可以给与函数或类访问私有属性的权限

friend void friendVisit(Building &b);  // 全局函数做友元
friend class GoodFriend;  // 类做友元
friend void GoodFriend::visit();  // 成员函数做友元

运算符重载

为自定义数据定义运算符

注意:

  • 运算符重载的函数也可以重载
  • 内置数据类型的运算符不能改变
  • 不要滥用运算符重载

加号运算符(+)

// 全局函数重载 + 运算符
Person operator+(Person& p1, Person& p2)
{
    // 运算符重载的实现
}

// 成员函数重载 + 运算符
Person operator+(Person& p)
{
    // 运算符重载的实现
}

比较运算符(==或!=)

  • 与加号运算符类似,但是返回的是 bool 类型的值

左移运算符(<<)

// 只能通过全局函数重载,因为cout在运算符左侧
ostream& operator<<(ostream& cout, Person& p)
{
    cout << p.age << " " << p.name;
    return cout;  // 为了可以连续输出
}

自增运算符(++)

// 成员函数重载前置自增
MyInt& operator++()
{
    num++;
    return *this;
}

// 成员函数重载后置自增
MyInt operator(int)
{
    MyInt temp = *this;
    num++;
    return temp;
}

赋值运算符(=),用来解决浅拷贝问题

// 成员函数重载赋值运算符
Person& operator=(Person& p)
{
    // 处理自身内存占用
    if(this!=nullptr)
    {
        delete this->m_age;
        this->m_age = nullptr;
    }
    this->m_age = new int(*p.m_age);
    return *this;  // 为了连续赋值
}

函数调用运算符,又称仿函数(())

// 成员函数重载函数调用运算符
void operator()(string text)
{
    cout << text << end;
}

// 匿名对象调用
cout << MyAdd()(100, 100) << endl;