~ cat test.cpp #include <iostream> using namespace std; const double PI = 3.14; class Circle { public: // 公共权限 // 求圆周长 double calcPerimeter() { return 2 * PI * r; } // 半径 int r; }; void test() { Circle c1; // 创建一个实例 // c1 半径赋值 c1.r = 100; // 求圆周长 cout << "c1 perimeter = " << c1.calcPerimeter() << endl; } int main() { test(); }
~ g++ test.cpp -o test && ./test c1 perimeter = 628
概述
C++中 OOP 思想来源于现实, 是对现实事物的抽象模拟, 具体来说, 当我们创建对象的时候这个对象应该有一个初始状态, 当对象销毁之前应该销毁自己创建的一些数据。
类和对象的基本概念
C 和 C++ 中 struct 区别
类的封装
类成员的权限
private: 私有属性
public: 共有属性
protected: 保护属性
静态成员
在类定义中, 它的成员 (包括成员变量和成员函数), 这些成员可以用关键字 static 声明为静态的, 称为静态成员.
不管这个类创建了多少个对象, 静态成员只有一个拷贝, 这个拷贝被所有属于这个类的对象共享.
静态成员变量
在一个类中, 若将一个成员变量声明为 static, 这种成员称为静态成员变量, 与一般的数据成员不同, 无论建立了多少个对象, 都只有一个静态数据的拷贝, 静态成员变量, 属于某个类, 所有对象共享.
静态变量, 是在编译阶段就分配空间, 对象还没有创建时, 就已经分配空间。
- 静态成员变量必须在类中声明,在类外定义 (初始化).
- 静态数据成员不属于某个对象, 在为对象分配空间中不包括静态成员所占空间。
- 静态数据成员可以通过类名或者对象名来引用。
- 静态成员也是有访问权限的, 私有权限类外访问不到.
~ cat test.cpp #include <iostream> using namespace std; class Person { public: static int m_A; }; int Person::m_A = 0; void test() { Person p1; cout << p1.m_A << endl; Person p2; p2.m_A = 100; cout << p1.m_A << endl; cout << Person::m_A << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test 0 100 100
静态成员函数
语法和使用类似于静态成员变量
区别在于
静态成员函数, 可以访问静态成员变量, 但不能访问非静态成员变量
对象的构造和析构
初始化和清理
构造函数和析构函数会被编译器自动调用, 完成对象初始化和对象清理工作.
无论你是否真欢, 对象的初始化和清理工作是输译器强制我们要做的事情, 即使你不提供初始化操作和清理操作, 输译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事, 所以输写类就应该顺便提供初始化函数。
为什么初始化操作是自动调用而不是手动调用? 既然是必须操作,那么自动调用会更好, 如果靠程序员自觉, 那么就会存在遗漏初始化的情况出现。
构造函数和析构函数必须要声明在全局作用域.
~ cat test.cpp #include <iostream> using namespace std; class Person { public: // 构造函数和析构函数必须要声明在全局作用域 // 构造函数: // 没有返回值, 不用写 void // 函数名与类名相同 // 可以有参数, 可以重载 // 构造函数由编译器自动调用一次 无需手动调用 Person() { cout << "Person 的构造函数调用" << endl; } // 析构函数: // 没有返回值 不用写 void // 函数名与类名相同, 函数名前加 ~ // 不可以有参数, 不可以重载 // 析构函数也是由编译器自动调用一次, 无需手动调用 ~Person () { cout << "Person 的析构函数调用" << endl; } }; void test() { Person p; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test Person 的构造函数调用 Person 的析构函数调用
实例被创建的时候会调用构造函数, 实例被销毁时会调用析构函数.
上例 test 函数中, 程序执行 Person p 时会调用构造函数, main 函数中 test 函数被执行完, 就会调用析构函数 (因为 实例 p 是创建在栈中的).
构造函数 (初始化)
构造函数的主要作用在于创建对象时为对象的成员属性赋值, 构造函数由编译器自动调用, 无需手动调用.
// 构造函数和析构函数必须要声明在全局作用域
// 构造函数:
// 没有返回值, 不用写 void
// 函数名与类名相同
// 可以有参数, 可以重载
// 构造函数由编译器自动调用一次 无需手动调用
构造函数的分类
按照参数分类
无参 (默认) 和有参
class Person +Person() { +~Person() public: Person() ▼ functions { main() cout << "Person 的默认构造函数调用" << endl; test() } ~ ~ Person(int a) ~ { ~ cout << "Person 的有参构造函数调用" << endl; ~ } ~ ~ };
按照类型分类
普通构造函数和拷贝构造函数
拷贝构造函数的写法: Person(const Person &p)
- const 修饰是为了避免修改到本体
- & 引用修饰是为了避免传值方式导致递归调用 (值传递的本质就是在调用拷贝)
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person() { cout << "Person 的默认构造函数调用" << endl; } Person(int age) { cout << "Person 的有参构造函数调用" << endl; m_age = age; } // 拷贝构造函数 Person(const Person &p) { cout << "Person 的拷贝构造函数调用" << endl; m_age = p.m_age; } int m_age; }; void test() { Person p(18); Person p2(p); cout << "p2.m_age: " << p2.m_age << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test Person 的有参构造函数调用 Person 的拷贝构造函数调用 p2.m_age: 18
拷贝构造函数的调用时机
// 1. 用已经创建好的对象来初始化新的对象 void test() { Person p1(18); Person p2 = Person(p1); }
// 2. 值传递方式给函数参数传值 void doWork(Person p) // 值传递的本质就是在调用它的拷贝构造函数 {} void test() { Person p1(100); doWork(p1); }
// 3. 以值的方式返回局部对象 Person doWork2() { Person p; // 这里返回的 p 是依据 Person p 中的 p 临时创建出来的, 可以看作是一个匿名对象 // Person p 中的 p 在 doWork2 函数执行完后会被释放, return p 中的 p 在函数执行完后, 返回给调用者 return p; } void test() { Person p = doWork2(); }
构造函数的调用
括号法
Person p;
Person p1(10);
Person p2(p);
注意事项:
不要用括号法调用无参构造函数, 编译器会误认为是函数声明 (Person p();).
显示法
Person p3 = Person(10);
Person p4 = Person(p3);
Person(10); // 匿名对象, 特点: 当前行执行完立即释放.
注意事项:
- Person(10); // 匿名对象, 特点: 当前行执行完立即释放.
- 不要用拷贝构造函数初始化匿名对象 (Person(p3); // 编译器认为是 Person p3 对象实例化, 如果已经之前有 p3, 就会报 p3 重定义错误).
隐式法
隐式类型转换, 可读性低
Person p5 = 10; // Person p5 = Person(10); Person p5 = p5; // Person p5 = Person(p5);
构造函数的调用规则
编译器会给一个类至少添加3个函数:
- 默认构造函数 (空实现)
- 析构函数 (空实现)
- 拷贝构造函数 (值拷贝)
如果我们自己提供了有参构造函数, 编译器就不会提供默认构造函数, 但依然会提供拷贝构造函数
如果我们自己提供了拷贝构造函数, 编译器就不会提供其他构造函数
构造函数的初始化列表
可以利用初始化列表语法 对类中的属性进行初始化
class Person { public: //Person() :m_A(10), m_B(20), m_C(30) //{} Person(int a, int b, int c) :m_A(a), m_B(b), m_C(v) {} int m_A; int m_B; int m_C; }; void test() { Person p(10, 20, 30); }
explicit 关键字
用途: 防止利用隐式类型转换方式来构造对象
class MyString { public: explicit MyString(int len) { } }; void test() { MyString str1(10); MyString str2 = MyString(100); MyString str3 = 10; // 当有 explicit 修饰时, 隐式方式会报错 }
析构函数 (清理)
析构函数的主要作用在于对象销毁前系统自动调用, 执行一些清理工作.
// 构造函数和析构函数必须要声明在全局作用域
// 析构函数:
// 没有返回值 不用写 void
// 函数名与类名相同, 函数名前加 ~
// 不可以有参数, 不可以重载
// 析构函数也是由编译器自动调用一次, 无需手动调用
~ cat test.cpp #include <iostream> using namespace std; class Person { public: // 构造函数和析构函数必须要声明在全局作用域 // 析构函数: // 没有返回值 不用写 void // 函数名与类名相同, 函数名前加 ~ // 不可以有参数, 不可以重载 // 析构函数也是由编译器自动调用一次, 无需手动调用 ~Person () { cout << "Person 的析构函数调用" << endl; } }; void test() { Person p; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test Person 的析构函数调用
动态对象创建
当我们创建数组的时候, 总是需要提前预定数组的长度, 然后编译器分配预定长度的数组空间, 在使用数组时, 会有这样的问题, 数组也许空间太大了, 浪费空间, 也许空间不足, 所以对于数组来讲, 如果能根据需要来分配空间大小再好不过。
所以动态的意思意味着不确定性。。
为了解决这个普遍的编程问题, 在运行中可以创建和销毁对象是最基本的要求, 当然 C 早就提供了动态内存分配 (dynamic memory allocation), 函数 malloc 和 free 可以在运行时从堆中分配存储单元。
然而这些函数在 C++ 中不能很好的运行, 因为它不能帮我们完成对象的初始化工作。
对象创建
当创建一个 C++ 对象时会发生两件事
- 1. 为对象分配内存
- 2.调用构造函数来初始化那块内存
C 动态分配内存方法
class Person { public: Person() { mAge = 20; pName = (char *)malloc(strlen("john") + 1); strcpy(pName, "john"); } void Init() { mAge = 20; pName = (char *)malloc(strlen("john") + 1); strcpy(pName, "john"); } void Clean() { if (pName != NULL) free(pName); } int mAge; char *pName; } int main() { // 分配内存 Person *person = (Person *)malloc(sizeof(Person)); if (person == NULL) return 0; // 调用初始化函数 person -> Init(); // 清理对象 person -> Clean(); // 释放 person 对象 free(person); return 0; }
问题:
- 1) 程序员必须确定对象的长度。
- 2) malloc 返回一个 void* 指针, C++ 不允许将 void* 赋值给其他任何指针, 必须强转。
- 3) malloc 可能申请内存失败, 所以必须判断返回值来确保内存分配成功。
- 4) 用户在使用对象之前必须记住对他初始化, 构造函数不能显示调用初始化 (构造函数是由编译器调用), 用户有可能忘记调用初始化函数。
new 运算符
Person *person = new Person;
相当于:
Person *person = (Person *)malloc(sizeof(Person)); if (person == NULL) { return 0; } person -> Init(); // 构造函数
new 操作符能确定在调用构造函数初始化之前内存分配是成功的, 所以不用显示确定调用是否成功.
现在我们发现在堆里创建对象的过程变得简单了, 只需要一个简单的表达式, 它带有内置的长度计算、类型转换和安全检查, 这样在堆创建一个对象和在栈里创建对象一样简单.
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person() { cout << "call default constructor func ..." << endl; } ~Person() { cout << "call destruct func..." << endl; } }; void test() { Person *p = new Person; delete p; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test call default constructor func ... call destruct func..
malloc、free 与 new、delete 的区别
malloc 和 free 是库函数, new 和 delete 是运算符
malloc 不会调用构造函数, new 会调用构造函数
malloc 返回 void *, C++ 下需要强转, new 返回创建的对象的指针
注意事项
1. 不要用 void * 去接收 new 出来的对象, 利用 void * 无法调用析构函数
void test() { void *p = new Person; delete p; }
不会走析构函数, 因为 void * 的空间大小未知
2. 利用 new 开辟数组
void test() { int *pInt = new int[10]; // 堆区开辟数组, 一定会调用默认构造函数 Person *pPerson = new Person[10]; // 释放数组需要加 [] delete [] pPerson; // 栈上开辟数组, 可以没有默认构造函数 Person pArray[10] = {Person(10), Person(10)}; }
delete 运算符
new 表达式的反面是 delete 表达式, delete 表达式先调用析构函数, 然后释放内存。正如 new 表达式返回一个指向对象的指针一样, delete 需要一个对象的地址。
delete 只适用于由 new 创建的对象。
如果使用一个由 malloc 或者 calloc 或者 realloc 创建的对象使用 delete, 这个行为是未定义的。因为大多数 new 和 delete 的实现机制都使用了 malloc 和 free, 所以很可能没有遇用析构函数就释放了内存。
如果正在删除的对象的指针是 NULL 将不发生任何事, 因此建议在删除指针后, 立即把指针赋值为 NULL, 以免对它删除两次, 对一些对象删除两次可能会产生某些问题。
单例模式
静态成员实现单例模式
单例模式是一种常用的软件设计模式, 在它的核心结构中只包含一个被称为单例的特殊类、通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问, 从而方便对实例个数的控制并节约系统资源, 如果希望在系统中某个类的对象只能存在一个, 单例模式是最好的解决方案。
单例模式在编译阶段就会分配内存
如果在单例模式的构造函数中打印内容, 会优先于 main 函数执行
单例类不用管释放的问题
~ cat test.cpp
#include <iostream>
using namespace std;
class ChairMan
{
public:
// 避免被修改, 所以只提供获取接口
static ChairMan * getInstance()
{
return singleMan;
}
private:
// 将构造函数私有化, 不可以创建多个对象
ChairMan() {};
// 将拷贝构造函数私有化, 避免通过拷贝构造方式创建
ChairMan(const ChairMan &p) {};
private:
// 唯一实例指针
static ChairMan *singleMan;
};
// 可以看作是在类内操作的, 所以会走私有的构造函数
ChairMan * ChairMan::singleMan = new ChairMan;
void test()
{
ChairMan * c1 = ChairMan::getInstance();
ChairMan * c2 = ChairMan::getInstance();
if (c1 == c2)
{
cout << "c1 == c2" << endl;
}
else
{
cout << "c1 != c2" << endl;
}
}
int main()
{
test();
return 0;
}
~ g++ test.cpp -o test && ./test c1 == c2
C++ 面向对象模型
成员变量和函数的存储
在 c 语言中, 数据和函数是分开来声明的, 也就是说, 语言本身并没有支持 “数据” 和 “函数” 之间的关联性, 我们把这种程序方法称为 “程序性的”, 由一组 “分布在各个以功能为导航的函数中” 的算法驱动, 它们处理的是共同的外部数据。
C++ 实现了 “封装”, 那么数据 (成员属性) 和操作 (成员函数) 是什么样的呢?
- 类中的 “数据” 和 “处理数据的操作 (函数)” 是分开存储的。
- c++ 中的非静态数据成员直接内含在类对象中, 就像 c struct 一样。
- 成员函数 (member function) 虽然内含在 class 声明之内, 却不出现在对象中。 每一个非内联成员函数 (non-inline member function) 只会诞生一份函数实例。
- 对象内存大小计算类似于结构体 struct.
- 空类的内存大小是 1个字节.
空对象
空类大小是 1, 原因是每个对象都应该在内存上有独一无二的地址, 因此给空对象分配一个字节的空间.
~ cat test.cpp #include <iostream> using namespace std; class Person {}; void test() { Person p1; cout << "sizeof = " << sizeof(p1) << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test sizeof = 1
this 指针
this 指针工作原理
通过上例我们知道, C++ 的数据和操作也是分开存储, 并且每一个非内联成员函数 (non-inline member function) 只会诞生一份函数实例, 也就是说多个同类型的对象会共用一块代码
那么问题是: 这一块代码是如何区分那个对象调用自己的呢?
对象1 对象2 对象10 --------- ---------- ----------- | 数据1 | | 数据2 | ... | 数据10 | --------- --------- ----------- ------------------------------------------ | 公用函数代码 | ------------------------------------------
C++ 通过提供特殊的对象指针, this 指针, 解决上述问题. this 指针指向被调用的成员函数所属的对象.
C++ 规定, this 指针是隐含在对象成员函数内的一种指针, 当一个对象被创建后。它的每一个成员函数都含有一个系统自动生成的隐含指针 this, 用以保存这个对象的地址。也就是说虽然我们没有写上 this 指针, 编译器在编译的时候也是会加上的, 因此 this 也称为 “指向本对象的指针”, this 指针并不是对象的一部分, 不会影响 sizeof(对象) 的结果。this 指针是 C++ 实现封装的一种机制它将对象和该对象调用的成员函数连接在一起, 在外部看来, 每一个对象都拥有自己的函数成员, 一般情况下, 并不写 this, 而是让系统进行默认设置。
this 指针永远指向当前对象.
成员函数通过 this 指针即可知道操作的是哪个对象的数据. this 指针是一种隐含指针, 它隐含于每个类的非晶态成员函数中, this 指针无需定义, 直接使用即可.
注意: 静态成员函数内部没有 this 指针, 静态成员函数不能操作非静态成员变量.
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person (int age) { // 解决名称冲突 this -> age = age; } // 以引用的方式返回, 得到的是本体 Person & personAddPerson(Person &p) { this -> age += p.age; return *this; // *this 就是本体 } int age; }; void test() { Person p1(1); Person p2(10); cout << "p1 age: " << p1.age << endl; p1.personAddPerson(p2).personAddPerson(p2); // 链式编程 cout << "p1 age: " << p1.age << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test p1 age: 1 p1 age: 21
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person (int age) { // 解决名称冲突 this -> age = age; } // 以值的形式返回, 会调用拷贝构造函数, 创建一个新的对象, 这个对象不是本体 Person personAddPerson(Person &p) { this -> age += p.age; return *this; // *this 就是本体 } int age; }; void test() { Person p1(1); Person p2(10); cout << "p1 age: " << p1.age << endl; p1.personAddPerson(p2).personAddPerson(p2); // 链式编程 cout << "p1 age: " << p1.age << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test p1 age: 1 p1 age: 11
空指针访问成员函数
~ cat test.cpp #include <iostream> using namespace std; class Person { public: void showClass() { cout << "I am a class" << endl; } void showAge() { if (this == NULL) { return; } // NULL -> age; 不允许, 报错 cout << "age = " << this -> age << endl; } int age; }; void test() { Person *p = NULL; p -> showClass(); p -> showAge(); } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test test.cpp:15:17: warning: 'this' pointer cannot be null in well-defined C++ code; comparison may be assumed to always evaluate to false [-Wtautological-undefined-compare] if (this == NULL) ^~~~ ~~~~ 1 warning generated. I am a class
常函数和常对象
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person(int age) { this -> age = age; } // 常函数, 修饰成员函数中的 this 指针, 让指针指向的值不可以修改 void showPerson() const { // this 指针的本质: Person * const this // 常函数中, this 指针的本质: const Person * const this // age = 100; // 不允许 m_A = 100; // 允许, 因为有 mutable 修饰 cout << "age: " << this -> age << endl; } void func() { age = 100; } int age; mutable int m_A; // 常函数中有些特殊的属性依然想修改, 可以加入关键字 mutable }; void test() { // 常对象 const Person p1(10); // p1.age = 10; // 不允许修改 p1.m_A = 100; // 可以修改 p1.showPerson(); // 允许 // p1.func(); // 不允许, 常对象只能调用常函数 } int main() { test(); return 0; }
友元
类的主要特点之一是数据隐藏, 即类的私有成员无法在类的外部 (作用域之外) 访问。但是, 有时候需要在类的外部访问类的私有成员, 怎么办?
解决方法是使用友元函数, 友元函数是一种特权函数, C++ 允许这个特权函数访问私有成员, 这一点从现实生活中也可以很好的理解:
比如你的家, 有客厅, 有你的卧室, 那么你的客厅是 Public 的, 所有来的客人都可以进去, 但是你的卧室是私有的, 也就是说只有你能进去, 但是呢, 你也可以允许你的闺蜜好基友进去。
程序员可以把一个全局函数、某个类中的成员函数, 甚至整个类声明为友元。
友元语法
friend 关键字只出现在声明处
全局函数做友元函数
friend void goodGay(Building * building);
~ cat test.cpp #include <iostream> using namespace std; class Building { // 利用 friend 关键字让全局函数 goodGay 作为本类的好朋友, 可以访问私有成员 friend void goodGay(Building * building); public: Building () { this -> m_SittingRoom = "sitting room"; this -> m_BedRoom = "bed room"; } public: string m_SittingRoom; private: string m_BedRoom; }; void goodGay(Building * building) { cout << "good gay is visiting " << building -> m_SittingRoom << endl; cout << "good gay is visiting " << building -> m_BedRoom << endl; } void test() { Building building; goodGay(&building); } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test good gay is visiting sitting room good gay is visiting bed room
类作为友元类
friend class GoodGay;
~ cat test.cpp #include <iostream> using namespace std; class Building; class GoodGay { public: GoodGay(); void visit(); Building * m_building; }; class Building { friend class GoodGay; public: Building(); string m_SittingRoom; private: string m_BedRoom; }; Building::Building() { this -> m_SittingRoom = "sitting room"; this -> m_BedRoom = "bed room"; } GoodGay::GoodGay() { this -> m_building = new Building; } void GoodGay::visit() { cout << "good gay is visiting " << this -> m_building -> m_SittingRoom << endl; cout << "good gay is visiting " << this -> m_building -> m_BedRoom << endl; } void test() { GoodGay gg; gg.visit(); } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test good gay is visiting sitting room good gay is visiting bed room
类中的成员函数作为友元函数
~ cat test.cpp #include <iostream> using namespace std; class Building; class GoodGay { public: GoodGay(); void visit(); // 可以访问 build 的私有 void visit2(); // 不可以访问 build 的私有 Building * m_building; }; class Building { friend void GoodGay::visit(); public: Building(); string m_SittingRoom; private: string m_BedRoom; }; Building::Building() { this -> m_SittingRoom = "sitting room"; this -> m_BedRoom = "bed room"; } GoodGay::GoodGay() { this -> m_building = new Building; } void GoodGay::visit() { cout << "good gay is visiting " << this -> m_building -> m_SittingRoom << endl; cout << "good gay is visiting " << this -> m_building -> m_BedRoom << endl; } void test() { GoodGay gg; gg.visit(); } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test good gay is visiting sitting room good gay is visiting bed room
运算符重载
基本概念
运算符重载, 就是对已有的运算符重新进行定义, 赋予其另一种功能, 以适应不同的数据类型.
在 C++ 中, 可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义, 只是函数的名字由关键字 operator 及其紧跟的运算符组成, 差别仅此而已, 它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时, 就会调用这个函数。
定义重载的运算符就像定义函数, 只是该函数的名字是 operator@, 这里的 @ 代表了被重载的运算符, 函数的参数中参数个数取决于两个因素。
- 运算符是一元 (一个参数) 的还是二元 (两个参数);
- 运算符被定义为全局函数 (对于一元是一个参数, 对于二元是两个参数) 还是成员函数 (对于一元没有参数, 对于二元是一个参数 - 此时该类的对象用作左耳参数)
加号运算符重载
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person () {}; Person(int a, int b): m_A(a), m_B(b) {} Person operator+(Person &p) { Person temp; temp.m_A = this -> m_A + p.m_A; temp.m_B = this -> m_B + p.m_B; return temp; } int m_A; int m_B; }; void test() { Person p1(10, 10); Person p2(20, 20); Person p3 = p1 + p2; // Person p3 = p1.operator+(p2); // 本质 cout << "p3.m_A = " << p3.m_A << endl; cout << "p3.m_B = " << p3.m_B << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test p3.m_A = 30 p3.m_B = 30
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person () {}; Person(int a, int b): m_A(a), m_B(b) {} int m_A; int m_B; }; Person operator+(Person &p1, Person &p2) { Person temp; temp.m_A = p1.m_A + p2.m_A; temp.m_B = p1.m_B + p2.m_B; return temp; } void test() { Person p1(10, 10); Person p2(20, 20); Person p3 = p1 + p2; // Person p3 = operator+(p1, p2); // 本质 cout << "p3.m_A = " << p3.m_A << endl; cout << "p3.m_B = " << p3.m_B << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test p3.m_A = 30 p3.m_B = 30
左移运算符重载
不要滥用运算符重载
不要对内置数据类型进行重载
对于自定义数据类型, 不可以直接用 cout << 输出, 需要重载左移运算符
如果利用成员函数重载, 无法实现让 cout 在左侧, 因此用全局函数实现左移运算符
ostream& operator<<(ostream &out, Person &p);
访问私有成员: friend ostream& operator<<(ostream &out, Person &p);
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person () {}; Person(int a, int b): m_A(a), m_B(b) {} int m_A; int m_B; }; ostream& operator<<(ostream &out, Person &p) { cout << "m_A = " << p.m_A << " m_B = " << p.m_B; return out; } void test() { Person p1(10, 10); cout << p1 << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test m_A = 10 m_B = 10
前置后置运算符重载
// 前置重载, 返回引用 xxx& operator++() {}; // 后置重载, 返回值 xxx operator++(int) {}; // 参数是一个类型占位符
优先使用 ++ 和 -- 的标准形式, 优先调用前置 ++。
如果定义了 ++c, 也要定义 c++, 递增操作符比较麻烦, 因为他们都有前缀和后缀形式, 而两种语义略有不同, 重载 operator++ 和 operator-- 时应该模仿他们对应的内置操作符。
对于 ++ 和 -- 而言,
- 后置形式是先返回, 然后对象 ++ 或者 …, 返回的是对象的原值,
- 前置形式, 对象先 ++ 或 --, 返回当前对象, 返回的是新对象.
调用代码时候, 要优先使用前缀形式, 除非确实需要后缀形式返回的原值, 前缀和后缀形式语义上是等价的, 输入工作量也相当, 只是效率经常会略高一些, 由于前缀形式少创建了一个临时对象。
~ cat test.cpp #include <iostream> using namespace std; class MyInter { public: MyInter() { m_Num = 0; } // 前置 ++ 重载 MyInter& operator++() { this -> m_Num++; return *this; } // 后置 ++ 重载 MyInter operator++(int) { // 先记录初始状态 MyInter temp = *this; this -> m_Num++; // 局部变量, 不能返回引用 return temp; } int m_Num; }; ostream& operator<<(ostream& cout, MyInter& myInt) { cout << myInt.m_Num; return cout; } void test1() { MyInter myInt; cout << ++myInt << endl; cout << myInt << endl; } void test2() { MyInter myInt; //cout << myInt++ << endl; cout << myInt << endl; myInt++; cout << myInt << endl; } int main() { test1(); test2(); return 0; }
~ g++ test.cpp -o test && ./test 1 1 0 1
指针运算符重载
利用智能化指针管理 new 出来的 person 的释放操作
设计 SmartPoint 智能指针类, 内部维护 Person*, 在析构时候释放堆区 new 出来的 person 对象.
// 重载 -> 运算符
Person * operator->() {}
// 重载 * 运算符
Person& operator*() {}
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person(int age) { cout << "Person 的有参构造调用" << endl; this -> m_Age = age; } void showAge() { cout << "age = " << this -> m_Age << endl; } ~Person() { cout << "Person 的析构调用" << endl; } int m_Age; }; class SmartPoint { public: SmartPoint(Person *person) { this -> m_person = person; } // 重载 -> 运算符 Person * operator->() { return this -> m_person; } // 重载 * 运算符 Person& operator*() { return *this -> m_person; } ~SmartPoint() { if (this -> m_person) { delete this -> m_person; this -> m_person = NULL; } } private: Person *m_person; }; void test() { Person *person = new Person(18); person -> showAge(); (*person).showAge(); delete person; // 利用智能化指针管理 new 出来的 person 的释放操作 SmartPoint mp = SmartPoint(new Person(19)); mp -> showAge(); (*mp).showAge(); } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test Person 的有参构造调用 age = 18 age = 18 Person 的析构调用 Person 的有参构造调用 age = 19 age = 19 Person 的析构调用
赋值运算符重载
编译器默认给一个类4个函数: 默认构造(空实现)、 析构(空实现)、 拷贝构造(值拷贝)、 operator= (值拷贝)
如果累中有属性创建在堆区, 利用编译器提供的 = 赋值运算符会出现堆区内存重复释放的问题
解决方案: 利用深拷贝 重载 = 运算符
// 重载 = 运算符
Person& operator=(const Person &p)
~ cat test.cpp #include <iostream> using namespace std; // 编译器默认给一个类4个函数: 默认构造、 析构、 拷贝构造(值拷贝)、 operator= (值拷贝) class Person { public: Person(const char *name, int age) { this -> m_Name = new char[strlen(name) + 1]; strcpy(this -> m_Name, name); this -> m_Age = age; } // 重载 = 运算符 Person& operator=(const Person &p) { // 先判断原来堆区是否有内存, 如果有则先释放 if (this -> m_Name != NULL) { delete [] this -> m_Name; this -> m_Name = NULL; } this -> m_Name = new char[strlen(p.m_Name) + 1]; strcpy(this -> m_Name, p.m_Name); this -> m_Age = p.m_Age; return *this; } // 拷贝构造 Person(const Person &p) { this -> m_Name = new char[strlen(p.m_Name) + 1]; strcpy(this -> m_Name, p.m_Name); this -> m_Age = p.m_Age; } ~Person() { if (this -> m_Name != NULL) { delete [] this -> m_Name; this -> m_Name = NULL; } } char *m_Name; int m_Age; }; void test() { Person p1 = Person("Point", 10); Person p2 = Person("World", 19); Person p3 = Person("", 0); p3 = p2 = p1; Person p4 = p3; cout << "p1.m_Name: " << p1.m_Name << ", p1.m_Age: " << p1.m_Age << endl; cout << "p2.m_Name: " << p2.m_Name << ", p2.m_Age: " << p2.m_Age << endl; cout << "p3.m_Name: " << p3.m_Name << ", p3.m_Age: " << p3.m_Age << endl; cout << "p4.m_Name: " << p4.m_Name << ", p4.m_Age: " << p4.m_Age << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test p1.m_Name: Point, p1.m_Age: 10 p2.m_Name: Point, p2.m_Age: 10 p3.m_Name: Point, p3.m_Age: 10 p4.m_Name: Point, p4.m_Age: 10
重载 [] 运算符
int& operator[](int index);
int& MyArray::operator[](int index)
{ return this -> pAddress[index]; }
关系运算符重载
bool operator==(Person &p);
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person(string name, int age) { this -> m_Name = name; this -> m_Age = age; } bool operator==(Person &p) { if (this -> m_Name == p.m_Name && this -> m_Age == p.m_Age) { return true; } return false; } string m_Name; int m_Age; }; void test() { Person p1 = Person("Point", 10); Person p2 = Person("Point", 10); if (p1 == p2) { cout << "p1 == p2" << endl; } else { cout << "p1 != p2" << endl; } } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test p1 == p2
函数调用运算符的重载
~ cat test.cpp #include <iostream> using namespace std; class MyPrint { public: void operator()(string text) { cout << text << endl; } }; void test() { MyPrint myPrint; myPrint("hello point"); // 仿函数, 本质是一个对象 函数对象 MyPrint()("hello world"); // 匿名函数对象, 特点: 当前行执行完立即释放 } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test hello point hello world
不要重载 &&、||
不能模拟短路特性
不能重载 operator&& 和 operator|| 的原因是, 无法在这两种情况下实现内置操作符的完整语义, 说得更具体一些, 内置版本特殊之处在于: 内置版本的 && 和 || 首先计算左边的表达式, 如果这完全能够决定结果, 就无需计算右边的表达式了… 而且能够保证不需要, 我们都已经习惯这种方便的特性了。
我们说操作符重载其实是另一种形式的函数调用而已, 对于函数调用总是在函数执行之前对所有参数进行求值。
符号重载总结
=、[]、() 和 -> 操作符只能通过成员函数进行重载
<< 和 >> 只能通过全局函数配合友元函数进行重载
不要重载 && 和 || 操作符, 因为无法实现短路规则
运算符 建议使用 所有的一元运算符 成员 = () [] -> * 必须是成员 += -= *= ^= &= != %= >>= <<= 成员 其它二元运算符 非成员
继承和派生
继承概述
父类 (基类)
子类 (派生类)
class BaseClass {}; class SubClass: public BaseClass {};
语法
class 子类名: 继承方式 父类名
继承方式
三种继承方式:
- public: 公有继承
- private: 私有继承
- protected: 保护继承
从继承源上分:
- 单继承: 指每个派生类只直接继承一个基类
- 多继承: 只多个基类派生出一个派生类的继承关系
派生类访问控制
派生类继承基类, 派生类拥有基类中全部成员变量和成员方法 (除了构造和析构之外的成员方法), 但是在派生类中, 继承的成员并不一定能直接访问, 不同的继承方式会导致不同的访问权限。
继承中的对象模型
父类中的私有属性, 子类是继承下去了, 但子类访问不到, 是由编译器给隐藏了, 可以用一些开发工具看到
~ cat test.cpp #include <iostream> using namespace std; class Base { public: int m_A; protected: int m_B; private: int m_C; // 父类中的私有属性, 子类是继承下去了, 但子类访问不到, 是由编译器给隐藏了 }; class Son: public Base { public: int m_D; }; void test() { cout << "size of Son = " << sizeof(Son) << endl; } int main() { test(); }
~ g++ test.cpp -o test && ./test size of Son = 16
继承中的构造和析构顺序
父类中的构造、析构、operator= 是不会被子类继承下去的
编译器提供的
~ sources cat test.cpp #include <iostream> using namespace std; class Base { public: Base() { cout << "Base 的构造函数调用" << endl; } ~Base() { cout << "Base 的析构函数调用" << endl; } }; class Son: public Base { public: Son() { cout << "Son 的构造函数调用" << endl; } ~Son() { cout << "Son 的析构函数调用" << endl; } }; void test() { Son son; } int main() { test(); }
~ g++ test.cpp -o test && ./test Base 的构造函数调用 Son 的构造函数调用 Son 的析构函数调用 Base 的析构函数调用
~ cat test.cpp #include <iostream> using namespace std; class Other { public: Other() { cout << "Other 的构造函数调用" << endl; } ~Other() { cout << "Other 的析构函数调用" << endl; } }; class Base { public: Base() { cout << "Base 的构造函数调用" << endl; } ~Base() { cout << "Base 的析构函数调用" << endl; } }; class Son: public Base { public: Son() { cout << "Son 的构造函数调用" << endl; } ~Son() { cout << "Son 的析构函数调用" << endl; } Other other; }; void test() { Son son; // 先调用父类构造, 再调用其他成员构造, 才调用自身构造, 析构的顺序与构造相反 } int main() { test(); }
~ g++ test.cpp -o test && ./test Base 的构造函数调用 Other 的构造函数调用 Son 的构造函数调用 Son 的析构函数调用 Other 的析构函数调用 Base 的析构函数调用
~ cat test.cpp #include <iostream> using namespace std; class Base { public: Base(int a) { cout << "Base 的构造函数调用" << endl; } }; class Son: public Base { public: Son(int a=28): Base(a) // 利用初始化列表语法, 显示使用父类中的其他构造函数 { cout << "Son 的构造函数调用" << endl; } }; void test() { Son son(18); } int main() { test(); }
~ g++ test.cpp -o test && ./test Base 的构造函数调用 Son 的构造函数调用
继承中同名成员的处理
子类覆盖父类
~ cat test.cpp #include <iostream> using namespace std; class Base { public: Base() { cout << "Base 的构造函数调用" << endl; this -> m_A = 20; } int m_A; }; class Son: public Base { public: Son() { cout << "Son 的构造函数调用" << endl; this -> m_A = 10; } int m_A; }; void test() { Son son; cout << "son.m_A = " << son.m_A << endl; cout << "Base m_A = " << son.Base::m_A << endl; } int main() { test(); }
~ g++ test.cpp -o test && ./test Base 的构造函数调用 Son 的构造函数调用 son.m_A = 10 Base m_A = 20
成员函数也一样
但当子类重新定义了父类中的同名成员函数, 子类的成员函数会隐藏掉父类中所有重载版本的同名成员, 可以利用作用域显示指定调用
继承中同名静态成员
同上
特殊之处, 也可以通过类名的方式访问父类作用域下的静态成员变量
Son::Base::m_A
特殊之处, 也可以通过类名的方式访问父类作用域下的静态成员函数
Son::Base::func()
多继承
class Son: public Base1, public Base2
{};
当多继承的两个父类中有同名成员, 需要加作用域区分
菱形继承和虚继承
Animal Sheep Tuo SheepTuo
cat test.cpp #include <iostream> using namespace std; class Animal { public: int m_Age; }; class Sheep: public Animal {}; class Tuo: public Animal {}; class SheepTuo: public Sheep, public Tuo {}; void test() { SheepTuo st; st.Sheep::m_Age = 10; st.Tuo::m_Age = 20; cout << st.Sheep::m_Age << endl; cout << st.Tuo::m_Age << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test 10 20
菱形继承: SheepTuo 的 m_Age 从 Sheep 里继承了一份, 又从 Tuo 里继承了一份, 不清晰且浪费资源
可以利用虚继承来解决菱形继承的问题
~ cat test.cpp #include <iostream> using namespace std; class Animal { public: int m_Age; }; class Sheep: virtual public Animal {}; // 被 virtual 修饰后, 父类 Animal 称为虚基类 class Tuo: virtual public Animal {}; class SheepTuo: public Sheep, public Tuo {}; void test() { SheepTuo st; st.Sheep::m_Age = 10; st.Tuo::m_Age = 20; cout << st.Sheep::m_Age << endl; cout << st.Tuo::m_Age << endl; cout << st.m_Age << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test 20 20 20
加了虚继承后, m_Age 数据就只有一份, 从 Animal 中获取
Sheep 和 Tuo 继承到的 m_Age 是虚基类指针, 虚基类指针指向的是一个表 vbtable (虚基类表)
vbptr: virtual base pointer
vbtable: 记录偏移量, 通过偏移量可以找到唯一的一个 m_Age
多态
基本概念
多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
多态性 (polymorphism) 提供接口与具体实现之间的另一层隔离,从而将 ”what" 和 “how" 分离开来。多态性改善了代码的可读性和组积性, 同时也使创建的程序具有可扩展性。项目不仅在最初创建时期可以扩展, 而且当项目在需要有新的功能时也能扩展。
c++ 支持
- 编译时多态 (静态多态) 和
- 运行时多态 (动态多态),
运算符重载和函数重载就是编译时多态, 而派生类和虚函数实现运行时多态。
静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用, 在编译阶段就可以确定函数的调用地址, 并产生代码, 就是静态多态 (编译时多态), 就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定而需要在运行时才能决定, 这这就属于晚绑定 (动态多态, 运行时多态).
~ cat test.cpp #include <iostream> using namespace std; class Animal { public: void speak() { cout << "animal are speaking" << endl; }; }; class Cat: public Animal { public: void speak() { cout << "cat are speaking" << endl; } }; // 对于有父子关系的两个类 指针或者引用是可以直接转换的 void doSpeck(Animal &animal) // Animal &animal = cat; { // 如果地址早就绑定好了, 地址早绑定, 属于静态联编 animal.speak(); // 如果想调用小猫说话, 这个时候函数的地址就不能早就绑定好, 而是在运行阶段绑定函数地址 (动态联编) } void test() { Cat cat; doSpeck(cat); } int main() { test(); }
~ g++ test.cpp -o test && ./test animal are speaking
动态多态产生条件:
- 先有继承关系
- 父类中有虚函数
- 子类重写父类虚函数 (子类中 virtual 可加可不加)
- 父类的指针或引用 指向子类的对象
~ cat test.cpp #include <iostream> using namespace std; class Animal { public: virtual void speak() { cout << "animal are speaking" << endl; }; }; class Cat: public Animal { public: void speak() { cout << "cat are speaking" << endl; } }; // 对于有父子关系的两个类 指针或者引用是可以直接转换的 void doSpeck(Animal &animal) // Animal &animal = cat; { // 如果地址早就绑定好了, 地址早绑定, 属于静态联编 // 如果想调用小猫说话, 这个时候函数的地址就不能早就绑定好, 而是在运行阶段绑定函数地址 (动态联编) animal.speak(); } void test() { Cat cat; doSpeck(cat); } int main() { test(); }
~ g++ test.cpp -o test && ./test cat are speaking
虚函数原理解剖
~ cat test.cpp #include <iostream> using namespace std; class Animal { public: void speak() { cout << "animal are speaking"; } }; void test() { // 空类 cout << "sizeof Animal: " << sizeof(Animal) << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test sizeof Animal: 1
虚函数
~ cat test.cpp #include <iostream> using namespace std; class Animal { public: // 虚函数 virtual void speak() { cout << "animal are speaking"; } }; void test() { // 指针 vfptr cout << "sizeof Animal: " << sizeof(Animal) << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test sizeof Animal: 8
vfptr: 虚函数表指针 指向虚函数表 (vftable)
- v - virtual
- f - function
- ptr - pointer
虚函数表内部记录虚函数入口地址
纯虚函数和抽象类
如果虚函数的具体实现没有意义, 可以写成纯虚函数
// 利用多态实现计算器 class AbstractCalculator { public: // 纯虚函数 virtual int getResult() = 0; // 虚函数 // virtual int get Result() {return 0;} };
如果一个类中包含了纯虚函数, 那么这个类就无法实例化对象了, 这个类我们称为抽象类
抽象类的子类, 必须重写父类中的纯虚函数, 否则也属于抽象类
虚析构和纯虚析构
class Animal { public: // 如果子类中有指向堆区的属性, 那么要利用虚析构技术, 在 delete 的时候调用子类的析构函数, 否则不会调用子类的析构函数 virtual ~Animal() {} }; Class Cat: public Animal { public: ~Cat() {} };
class Animal { public: // 如果子类中有指向堆区的属性, 那么要利用虚析构技术, 在 delete 的时候调用子类的析构函数, 否则不会调用子类的析构函数 // virtual ~Animal() {} // 纯虚析构, 需要有声明, 也需要实现 // 如果一个类中有了纯虚析构函数, 那么这个类也变成了抽象类, 无法实例化对象 virtual ~Animal() = 0; }; Animal::~Animal() {} Class Cat: public Animal { public: ~Cat() {} };
其他
深浅拷贝的问题及解决
如果有属性开辟到堆区, Person p2(p); 编译器提供的默认拷贝构造函数, 只是做了简单的值拷贝, 在析构函数中会出现重复释放堆区内存的问题.
可以利用深拷贝解决浅拷贝的问题. 自己提供拷贝构造函数来实现深拷贝.
现象
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person(const char *name, int age) { m_Name = (char *)malloc(strlen(name) + 1); strcpy(m_Name, name); m_Age = age; } ~Person() { if (m_Name != NULL) { free(m_Name); m_Name = NULL; } } char *m_Name; int m_Age; }; void test() { Person p("Point", 18); cout << "name: " << p.m_Name << " age: " << p.m_Age << endl; Person p2(p); cout << "name: " << p2.m_Name << " age: " << p2.m_Age << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test name: Point age: 18 name: Point age: 18 test(19271,0x114550600) malloc: *** error for object 0x6000027a0040: pointer being freed was not allocated test(19271,0x114550600) malloc: *** set a breakpoint in malloc_error_break to debug [1] 19271 abort ./test
解决
~ cat test.cpp #include <iostream> using namespace std; class Person { public: Person(const char *name, int age) { m_Name = (char *)malloc(strlen(name) + 1); strcpy(m_Name, name); m_Age = age; } Person(const Person &p) { m_Name = (char *)malloc(strlen(p.m_Name) + 1); strcpy(m_Name, p.m_Name); m_Age = p.m_Age; } ~Person() { if (m_Name != NULL) { free(m_Name); m_Name = NULL; } } char *m_Name; int m_Age; }; void test() { Person p("Point", 18); cout << "name: " << p.m_Name << " age: " << p.m_Age << endl; Person p2(p); cout << "name: " << p2.m_Name << " age: " << p2.m_Age << endl; } int main() { test(); return 0; }
~ g++ test.cpp -o test && ./test name: Point age: 18 name: Point age: 18
向上/向下类型转换
父转子 -> 向下类型转换: 不安全, 寻址越界
子转父 -> 向上类型转换: 安全
如果发生多态: 转换永远是安全的
重写 重载 重定义
重载, 同一作用域的同名函数
- 1. 同一个作用域
- 2. 参数个数, 参数顺序, 参数类型不同
- 3. 和函数返回值, 没有关系
- 4. const 也可以作为重载条件
- do(const Teacher& t) {}
- do(Teacher& t) {}
重定义 (隐藏)
- 1. 有继承。
- 2. 子类 (派生类) 重新定义父类 (基类) 的同名成员 (非 virtual 函数)
重写 (覆盖)
- 1. 有继承。
- 2. 子类 (派生类) 重写父类 (基类) 的 virtual 函数
- 3. 函数返回值, 函数名字, 函数参数, 必须和基类中的虚函数一致
class A { public: // 同一作用域下, func1 函数重载 void func1() {} void func1(int a) {} void func1(int a, int b) {} void func2() {} virtual void func3() {} }; class B: public A { public: // 重定义基类的 func2 函数, 隐藏了基类的 func2 方法 void func2() {} // 重写基类的 func3 函数, 也可以覆盖基类 func3 virtual void func3() {} };