类和对象
OOP 的三大特性:封装、继承、多态。
struct
和 class
区别:
在 C++ 中 struct
和 class
唯一的区别就在于:默认的访问权限不同,struct
默认权限为公共,class
默认权限为私有。
相较于 C 中的 struct
,C++ 中的 struct
和
class
都可以定义自己的成员函数。
C++ 中的成员权限和继承:
public 继承 |
仍为 public 成员 |
仍为 protected 成员 |
不可见 |
protected 继承 |
变为 protected 成员 |
变为 protected 成员 |
不可见 |
private 继承 |
变为 private 成员 |
变为 private 成员 |
不可见 |
对象的初始化和清理
构造函数和析构函数
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造函数和析构函数,编译器会提供。编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名() {}
- 构造函数,没有返回值也不写
void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:~类名() {}
- 析构函数,没有返回值也不写
void
- 函数名称与类名相同,在名称前加上符号
~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
构造函数的分类及调用
两种分类方式:
按参数分为: 有参构造和无参构造
按类型分为: 普通构造和拷贝构造
注意:
- 如果不提供任何函数,编译器会提供至少 4
个函数:默认构造函数、拷贝构造函数、析构函数、
operator=()
.
- 如果自定义了一个构造函数,编译器不在提供默认构造函数。
- 如果自定义了一个拷贝构造函数,编译器不在提供默认的拷贝构造函数。
- 默认的拷贝构造函数是浅拷贝。
三种调用方式:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include <iostream>
using namespace std;
class Person { public: Person() { cout << "无参构造函数!" << endl; }
Person(int a) { age = a; cout << "有参构造函数!" << endl; }
Person(const Person &p) { age = p.age; cout << "拷贝构造函数!" << endl; }
~Person() { cout << "析构函数!" << endl; }
public: int age; };
int main() {
Person p;
Person p1(10);
Person p2 = Person(10); Person p3 = Person(p2);
Person p4 = 10; Person p5 = p4;
return 0; }
|
输出结果:
image
拷贝构造函数调用时机
C++ 中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象。
- 值传递的方式给函数参数传值。
- 以值方式返回局部对象。
即:旧对象初始化新对象。
以值传递返回局部对象在 GCC 中会有优化,可能看到不同的结果。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include <iostream>
using namespace std;
class Person { public: Person() { cout << "无参构造函数!" << endl; mAge = 0; } Person(int age) { cout << "有参构造函数!" << endl; mAge = age; } Person(const Person &p) { cout << "拷贝构造函数!" << endl; mAge = p.mAge; }
~Person() { cout << "析构函数!" << endl; }
public: int mAge; };
void test01() {
Person man(100); Person newman(man); Person newman2 = man;
}
void doWork(Person p1) {} void test02() { Person p; doWork(p); }
Person doWork2() { Person p1; cout << (int *)&p1 << endl; return p1; }
void test03() { Person p = doWork2(); cout << (int *)&p << endl; }
int main() {
test01(); test02(); test03();
return 0; }
|
输出结果:
image
C++ 默认增加的函数
默认情况下,C++ 编译器至少给一个类添加 3 个函数:
1.默认构造函数 2.默认析构函数
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++
不在提供默认无参构造,但是会提供默认拷贝构造。
- 如果用户定义拷贝构造函数,C++ 不会再提供其他构造函数。
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作。
深拷贝:在堆区重新申请空间,进行拷贝操作。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <iostream>
using namespace std;
class Person { public: Person() { cout << "无参构造函数!" << endl; }
Person(int age, int height) {
cout << "有参构造函数!" << endl;
m_age = age; m_height = new int(height); }
Person(const Person &p) { cout << "拷贝构造函数!" << endl; m_age = p.m_age; m_height = new int(*p.m_height); }
~Person() { cout << "析构函数!" << endl; if (m_height != nullptr) { delete m_height; } }
public: int m_age; int *m_height; };
int main() {
Person p1(18, 180);
Person p2(p1);
cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
return 0; }
|
输出结果:
image
如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
初始化列表
语法:
1
| 构造函数(): 属性1(值1),属性2(值2)...{}
|
注意:
- 初始化列表是先声明,在调用构造函数时定义并初始化,定义初始化的顺序和声明的顺序一致。
- 普通的构造函数是先定义,在赋值。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream>
using namespace std;
class Person { public: Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) {}
void PrintPerson() { cout << "mA:" << m_A << endl; cout << "mB:" << m_B << endl; cout << "mC:" << m_C << endl; }
private: int m_A; int m_B; int m_C; };
int main() {
Person p(1, 2, 3); p.PrintPerson();
return 0; }
|
输出结果:
image
类对象作为类成员
- 类中有多个对象时,构造的顺序是先构造里面的对象,再构造外面的对象。
- 类中有多个对象时,析构时顺序是先析构外面的对象,再析构里面的对象。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <iostream>
using namespace std;
class Phone { public: Phone(string name) { m_PhoneName = name; cout << "Phone构造" << endl; }
~Phone() { cout << "Phone析构" << endl; }
string m_PhoneName; };
class Person { public: Person(string name, string pName) : m_Name(name), m_Phone(pName) { cout << "Person构造" << endl; }
~Person() { cout << "Person析构" << endl; }
void playGame() { cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl; }
string m_Name; Phone m_Phone; };
int main() {
Person p("张三", "苹果X"); p.playGame();
return 0; }
|
输出结果:
image
静态成员
静态成员就是在成员变量和成员函数前加上关键字 static
.
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
示例 1:静态成员变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream>
using namespace std;
class Person { public: static int m_A;
private: static int m_B; };
int Person::m_A = 10; int Person::m_B = 10;
int main() {
Person p1; p1.m_A = 100; cout << "p1.m_A = " << p1.m_A << endl;
Person p2; p2.m_A = 200; cout << "p1.m_A = " << p1.m_A << endl; cout << "p2.m_A = " << p2.m_A << endl;
cout << "m_A = " << Person::m_A << endl;
return 0; }
|
输出结果:
image
示例 2:静态成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <iostream>
using namespace std;
class Person {
public:
static void func() { cout << "func调用" << endl; m_A = 100; }
static int m_A; int m_B;
private: static void func2() { cout << "func2调用" << endl; } };
int Person::m_A = 10;
int main() {
Person p1; p1.func();
Person::func();
return 0; }
|
输出结果:
image
explicit
explicit
修饰的构造函数不能在隐式转换中使用。
C++对象模型
动态对象创建
使用 malloc
和 free
函数去动态申请对象和释放申请的对象,不会调用构造函数和析构函数。
在 C++ 中建议使用 new
运算符和 delete
运算符进行动态对象的申请和释放。
语法:
1 2 3 4 5 6
| 类型 *p = new 构造器; delete p;
类型 *p = new 类型[size]; delete []p;
|
不要 delete
万能指针(void *
)
const
修饰的静态成员变量
const
修饰的静态成员变量保存在常量区,只读的,在内存中只有一份
const
修饰的静态成员变量可以在类内定义且初始化
const
修饰的静态成员变量可以通过类的作用域访问
const
修饰的静态成员变量可以通过对象访问
- 静态成员函数可以访问
const
修饰的静态成员变量
成员变量和成员函数分开存储
- 普通成员变量占用对象空间大小
- 静态成员变量不占用对象空间大小
- 普通成员函数不占用对象空间大小
- 静态成员函数不占用对象空间大小
在 C++
中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream>
using namespace std;
class Person { public: Person() { mA = 0; }
int mA;
static int mB;
void func() { cout << "mA:" << this->mA << endl; }
static void sfunc() {} };
int main() {
cout << sizeof(Person) << endl;
return 0; }
|
输出结果:
image
一个对象至少占 1 个字节,要不然获取不到其地址。
this
指针
this
指针是一个指向对象自己的指针。
空指针访问成员函数
C++ 中空指针也是可以调用成员函数的,但是也要注意有没有用到
this
指针。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream>
using namespace std;
class Person { public: void ShowClassName() { cout << "我是Person类!" << endl; }
void ShowPerson() { if (this == nullptr) { return; } cout << mAge << endl; }
public: int mAge; };
int main() {
Person *p = nullptr; p->ShowClassName(); p->ShowPerson();
return 0; }
|
输出结果:
image
const
修饰成员函数
- 在函数后面加上
const
,这个是一个常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字
mutable
后,在常函数中依然可以修改
上述规则对于非成员函数和成员变量也适用。
友元
友元可以让一个函数、类或者一个类的成员函数访问另外一个类的私有成员。
有 3 种友元:
全局函数做友元
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream>
using namespace std;
class Building { friend void goodGay(Building *building);
public: Building() { this->m_SittingRoom = "客厅"; this->m_BedRoom = "卧室"; }
public: string m_SittingRoom;
private: string m_BedRoom; };
void goodGay(Building *building) { cout << "好基友正在访问:" << building->m_SittingRoom << endl; cout << "好基友正在访问:" << building->m_BedRoom << endl; }
int main() {
Building b; goodGay(&b);
return 0; }
|
输出结果:
image
类做友元
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include <iostream>
using namespace std;
class Building;
class goodGay { public: goodGay(); void visit();
private: Building *building; };
class Building {
friend class goodGay;
public: Building();
public: string m_SittingRoom;
private: string m_BedRoom; };
Building::Building() { this->m_SittingRoom = "客厅"; this->m_BedRoom = "卧室"; }
goodGay::goodGay() { building = new Building; }
void goodGay::visit() { cout << "好基友正在访问:" << building->m_SittingRoom << endl; cout << "好基友正在访问:" << building->m_BedRoom << endl; }
int main() {
goodGay gg; gg.visit();
return 0; }
|
输出结果:
image
成员函数做友元
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #include <iostream>
using namespace std;
class Building;
class goodGay { public: goodGay(); void visit(); void visit2();
private: Building *building; };
class Building { friend void goodGay::visit();
public: Building();
public: string m_SittingRoom; private: string m_BedRoom; };
Building::Building() { this->m_SittingRoom = "客厅"; this->m_BedRoom = "卧室"; }
goodGay::goodGay() { building = new Building; }
void goodGay::visit() { cout << "好基友正在访问" << building->m_SittingRoom << endl; cout << "好基友正在访问" << building->m_BedRoom << endl; }
void goodGay::visit2() { cout << "好基友正在访问" << building->m_SittingRoom << endl; }
int main() {
goodGay gg; gg.visit();
return 0; }
|
输出结果:
image
继承
通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。
一个 B 类继承于 A 类,或称从类 A 派生类 B。这样的话,类 A 成为基类,
类 B 成为派生类
派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过过来的表现其共性,而新增的成员体现了其个性。
继承的语法
C++ 中的成员权限和继承方式:
public 继承 |
仍为 public 成员 |
仍为 protected 成员 |
不可见 |
protected 继承 |
变为 protected 成员 |
变为 protected 成员 |
不可见 |
private 继承 |
变为 private 成员 |
变为 private 成员 |
不可见 |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <iostream>
using namespace std;
class Animal { public: int age; void print() { cout << age << endl; } };
class Dog : public Animal { public: int tail_len; };
int main() { Dog d; return 0; }
|
继承中的对象模型
在 C++
编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <iostream>
using namespace std;
class Aclass { public: int mA; int mB; };
class Bclass : public Aclass { public: int mC; };
class Cclass : public Bclass { public: int mD; };
int main() { cout << "A size:" << sizeof(Aclass) << endl; cout << "B size:" << sizeof(Bclass) << endl; cout << "C size:" << sizeof(Cclass) << endl; return 0; }
|
输出结果:
image
非自动继承的函数
子类不会继承父类的构造函数 、析构函数和 operator=
函数。
继承中构造和析构顺序
子类对象在创建时会首先调用父类的构造函数,父类构造函数执行完毕后,才会调用子类的构造函数。
当父类构造函数有参数时,需要在子类初始化列表中显示调用父类构造函数。
析构函数调用顺序和构造函数相反。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #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; } };
int main() {
Son s;
return 0; }
|
输出结果:
image
继承同名成员处理方式
如果子类和父类有同名的成员变量和成员函数,继承时,父类的成员变量和成员函数会被隐藏
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <iostream>
using namespace std;
class Base { public: Base() { m_A = 100; }
void func() { cout << "Base - func()调用" << endl; }
void func(int a) { cout << "Base - func(int a)调用" << endl; }
public: int m_A; };
class Son : public Base { public: Son() { m_A = 200; }
void func() { cout << "Son - func()调用" << endl; }
public: int m_A; };
int main() {
Son s;
cout << "Son下的m_A = " << s.m_A << endl; cout << "Base下的m_A = " << s.Base::m_A << endl;
s.func(); s.Base::func(); s.Base::func(10);
return 0; }
|
输出结果:
image
继承同名静态成员处理方式
继承时,子类和父类有同名的静态成员函数或静态成员变量,父类中的静态成员函数或静态成员变量会被隐藏。
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <iostream>
using namespace std;
class Base { public: static void func() { cout << "Base - static void func()" << endl; } static void func(int a) { cout << "Base - static void func(int a)" << endl; }
static int m_A; };
int Base::m_A = 100;
class Son : public Base { public: static void func() { cout << "Son - static void func()" << endl; } static int m_A; };
int Son::m_A = 200;
int main() { cout << "通过对象访问:" << endl; Son s; s.func(); s.Base::func();
cout << "通过类名访问:" << endl; Son::func(); Son::Base::func();
Son::Base::func(100);
return 0; }
|
输出结果:
image
多继承
C++ 允许一个类继承多个类
语法:
1
| class 子类 : 继承方式 父类1, 继承方式 父类2...
|
多继承可能会引发父类中有同名成员出现,需要加作用域区分。
实际开发中不建议使用多继承
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include <iostream>
using namespace std;
class Base1 { public: Base1() { m_A = 100; }
public: int m_A; };
class Base2 { public: Base2() { m_A = 200; }
public: int m_A; };
class Son : public Base2, public Base1 { public: Son() { m_C = 300; m_D = 400; }
public: int m_C; int m_D; };
void test01() {}
int main() {
Son s; cout << "sizeof Son = " << sizeof(s) << endl; cout << s.Base1::m_A << endl; cout << s.Base2::m_A << endl;
return 0; }
|
输出结果:
image
菱形继承
菱形继承:两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <iostream>
using namespace std;
class Animal { public: int m_Age; };
class Sheep : virtual public Animal {}; class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
int main() {
SheepTuo st; st.Sheep::m_Age = 100; st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; cout << "st.m_Age = " << st.m_Age << endl;
return 0; }
|
输出结果:
image
虚继承的实现原理:
image
多态
多态的基本概念
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定:编译阶段确定函数地址
- 动态多态的函数地址晚绑定:运行阶段确定函数地址
多态满足条件:
多态使用条件:
重写:函数返回值类型、函数名、参数列表完全一致称为重写。
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:
1
| virtual 返回值类型 函数名 (参数列表)= 0;
|
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象。
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
虚析构语法:
- 虚析构或纯虚析构用来解决通过父类指针释放子类对象。
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构。
- 拥有纯虚析构函数的类也属于抽象类。