面向对象编程的三大特性:
- 封装
- 继承
- 多态
封装的要点
- 将属性和行为作为整体管理
- 对属性和行为进行权限控制
属性和行为
- 属性:成员属性、成员变量
- 行为:成员函数、成员方法
访问权限
值得注意的是,访问权限限定的是类,而不是对象。例如在拷贝构造函数中可以调用相同类,不同对象的私有成员
- public:类内可以访问,类外可以访问,子类中可以访问
- protected:类内可以访问,子类中可以访问,类外不可以访问
- private:类内可以访问,子类中不可以访问,类外不可以访问
将属性设为私有的好处
- 控制读写权限
- 可以检测数据的有效性
类的分文件编写
- 创建头文件(.h)和源文件(.cpp)
- 在头文件中编写成员变量和成员函数声明
- 在源文件中编写成员函数定义
- 在使用这个类时,要先引入包含这个类的头文件
构造函数和析构函数
- 构造函数:执行成员属性初始化,由编译器自动调用
- 析构函数:执行清理工作,在对象销毁前自动调用
构造函数语法
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);
- 当前行执行后,系统会回收匿名函数
- 不要利用拷贝构造函数初始化匿名对象,编译器会理解成多重定义
拷贝函数的调用时机
- 使用一个创建完的对象初始化新对象
- 以值传递的方式给函数参数传值
- 以值的方式返回局部对象
对象中默认存在的函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,构造体为空)
- 默认拷贝构造函数(对属性进行浅拷贝)
- 赋值运算符(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;