面向对象的三大特性为:封装、继承、多态
类的基本思想:数据抽象和封装
封装 (encapsulation) 将接口和实现分离
权限控制
public 类内可访问,类外可访问,子类可访问protected 类内可访问,类外不可访问,子类可访问private 类内可访问,类外不可访问,子类不可访问struct 和 class 的区别
struct 的默认权限是 public
class 的默认权限是 private
对象的初始化和清理 构造函数和析构函数 构造函数:主要作用在于创建对象时为对象的成员属性赋值,当类的对象被创建时,就会执行构造函数 析构函数:主要作用在于对象销毁前系统自动调用,执行清理工作 构造函数语法:类名(){}
函数名称和类名相同,无返回值 构造函数可以有参数,可重载 析构函数语法:~类名(){}
函数名称和类名相同,在前面加上~
析构函数不可以有参数,不能发生重载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Person {public : Person () { cout << "构造函数的调用" << endl; } ~Person () { cout << "析构函数的调用" << endl; } };void test () { Person P; }
构造函数的分类和调用 两种分类方式:按参数分为:有参构造和无参构造 按类型分为:普通构造和复制构造 三种调用方式: 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 class Person {public : Person () { cout << "Person的无参构造函数调用" << endl; } Person (int a) { age = a; cout << "Person的有参构造函数调用" << endl; } Person (const Person &p) { age = p.age; cout << "Person的复制构造函数调用" << endl; } ~Person () { cout << "Person的析构函数调用" << endl; } int age; };void test () { Person p1; Person p2 (10 ) ; Person p3 (p2) ; Person p4; Person p5 = Person (10 ); Person p6 = Person (p5); Person p7 = 10 ; Person p8 = p7; }
复制构造函数的使用时机 使用一个已经创建完毕的对象来初始化一个新对象 值传递的方式给函数参数传值 以值方式返回局部对象 (由于编译优化可能不会调用)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 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; } ~Person () { cout<<"Person析构函数调用" <<endl; } int m_Age; };void test01 () { Person p1 (20 ) ; Person p2 (p1) ; }void doWork (Person p) {}void test02 () { Person p; doWork (p); }Person doWork2 () { Person p1; return p1; }void test03 () { Person p = doWork2 (); }
构造函数调用规则 默认情况下,C++编译器至少给一个类添加4个函数
默认构造函数(无参,函数体为空) 默认析构函数(无参,函数体为空) 默认复制构造函数,对所有属性值进行复制 赋值运算符 operator=,对属性值进行复制 构造函数调用规则如下:
如果用户定义了有参构造函数,则默认无参构造函数不再提供 如果用户定义了复制构造函数,则其他构造函数都不会被提供 shallowCopy 和 deepCopy 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 class Person {public : Person (){} Person (int age, int height){ m_Age = age; m_Height = new int (height); } Person (const Person &p){ m_Age = p.m_Age; m_Height = new int (*p.m_Height); } ~Person (){ if (m_Height != NULL ){ delete m_Height; m_Height = NULL ; } }public : int m_Age; int * m_Height; };void test01 () { 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; }int main () { test01 (); return 0 ; }
总结:如果属性有在堆区开辟的,要自己提供复制构造函数,防止shallowCopy带来的问题
初始化列表 语法:构造函数(): 属性1(值1), 属性2(值2) ... {}
1 2 3 4 5 6 7 8 9 10 11 12 class Person {public : Person (int a, int b, int c): m_A (a), m_B (b), m_C (c) {}private : int m_A; int m_B; int m_C; };int main () { Person p (10 ,20 ,30 ) ; return 0 ; }
类对象作为类成员 1 2 3 4 5 class A {};class B { A a; };
构造顺序:先调用对象成员的构造,再调用本类构造
析构顺序:先析构本类,再析构对象成员
静态成员 在成员变量和成员函数前加上关键字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 34 class Person {public : static int m_A;private : static int m_B; };int Person::m_A = 100 ;int Person::m_B = 300 ;void test01 () { Person p1; cout<<p1.m_A<<endl; Person p2; p2.m_A = 200 ; cout<<p1.m_A<<endl; }void test02 () { Person p; cout<<p.m_A<<endl; cout<<Person::m_A<<endl; }
示例2:静态成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Person {public : static void func () { m_A = 100 ; cout<<"static void func调用" <<endl; } static int m_A = 10 ; };void test01 () { Person p; p.func (); Person::func (); }
C++对象模型和this指针 成员变量和成员函数分开存储 在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量 才属于类的对象上
空类的对象占1个字节
this指针 每一个非静态成员函数只会产生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分哪个对象调用自己的呢?
C++通过提供特殊的对象指针---- this
指针,解决上述问题
this
指针指向 被调用的成员函数所属的对象
this指针的特性:
隐含于每一个非静态成员函数之内 不需要定义,直接使用 本质是指向不可修改 的指针 this指针的用途:
当形参和成员变量同名时,可以使用 this
指针来区分 在类的非静态成员函数中返回对象本身 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Apple {public : Apple (int num){ this ->num=num; } Apple& myAdd (Apple& p) { num+=p.num; return *this ; } int num; };int main () { Apple a1 (10 ) ; Apple a2 (20 ) ; a2.myAdd (a1).myAdd (a1); cout<<a2.num<<endl; }
空指针访问成员函数 C++中空指针是可以调用成员函数的,但要注意有没有用到this指针
如果成员函数没有用到this指针,那么空指针可以直接访问成员函数
如果成员函数用到this指针,就要判断指针是否为空,防止崩溃
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Person {public : void showClassName () { cout<<"This is a Person class" <<endl; } void showAge () { if (this ==NULL ){return ;} cout<<"age= " <<m_Age<<endl; } int m_Age; };void test01 () { Person* p = NULL ; p->showClassName (); p->showAge (); }
常函数和常对象 常函数:
常对象:
声明对象前加const称该对象为常对象 常对象的属性不可修改,但是mutable属性仍然可以修改 常对象只能调用常函数 友元 允许一个函数或者类,访问该类中的私有成员
关键字: friend
友元的三种实现
全局函数做友元
类做友元
成员函数做友元
运算符重载 对已有的运算符进行重新定义,赋予其另一种功能,以适应不同的数据类型
加号运算符重载 作用:实现两个自定义数据类型相加的运算
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 class Building {public : Building (int a, int b):m_A (a), m_B (b){}; Building operator +(Building& b) const { Building temp (0 ,0 ) ; temp.m_A = m_A + b.m_A; temp.m_B = m_B + b.m_B; return temp; } int m_A, m_B; }; Building operator +(Building& b1, Building& b2) { Building temp (0 ,0 ) ; temp.m_A = b1.m_A + b2.m_A; temp.m_B = b1.m_B + b2.m_B; return temp; } Building operator +(Building& b, int a) { Building temp (0 ,0 ) ; temp.m_A = b.m_A + a; temp.m_B = b.m_B + a; return temp; }int main () { Building b1 (10 ,20 ) , b2 (20 ,10 ) ; Building b3 = b1 + b2; b3 = b3 + 10 ; cout<<"b3: " <<b3.m_A<<" " <<b3.m_B<<endl; }
注意: 对于内置数据类型的表达式的运算符是不能改变的
左移运算符重载 作用:可以输出自定义数据类型
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 #include <iostream> using namespace std;class Person { friend ostream& operator <<(ostream& cout, Person& p);public : Person (int a, int b) { m_A = a; m_B = b; } private : int m_A; int m_B; }; ostream& operator <<(ostream& cout, Person& p) { cout<<"m_A = " <<p.m_A<<" m_B = " <<p.m_B; return cout; }int main () { Person p (10 ,20 ) ; cout<<p<<" (<<重载测试)" <<endl; }
递增运算符重载 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 class MyInteger { friend ostream& operator <<(ostream& cout, MyInteger a);public : MyInteger () { m_Num =0 ; } MyInteger& operator ++(){ ++m_Num; return *this ; } MyInteger operator ++(int ){ MyInteger temp = *this ; m_Num++; return temp; }private : int m_Num; }; ostream& operator <<(ostream& cout, MyInteger a) { cout<<"m_Mum = " <<a.m_Num; return cout; }int main () { MyInteger a; cout << a << endl; cout << ++(++a)<<endl; cout << a++ <<endl; cout << a <<endl; }
赋值运算符重载 默认情况下,C++编译器至少给一个类添加4个函数
默认构造函数(无参,函数体为空) 默认析构函数(无参,函数体为空) 默认复制构造函数,对所有属性值进行复制 赋值运算符 operator=,对属性值进行复制 如果类中有属性指向堆区,做赋值操作时也会出现shallow/deep copy问题
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 class Person {public : Person (int age){ m_Age = new int (age); } ~Person (){ if (m_Age != NULL ){ delete m_Age; m_Age = NULL ; } } Person& operator =(Person& p){ if (m_Age != NULL ){ delete m_Age; m_Age = NULL ; } m_Age = new int (*p.m_Age); return *this ; } int * m_Age; };int main () { Person p1 (18 ) ; Person p2 (20 ) ; Person p3 (22 ) ; p1 = p2 = p3; cout<<"p1的年龄为:" <<*p1.m_Age<<endl; }
关系运算符重载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Person {public : Person (string name, int age){ m_Name = name; m_Age = age; } bool operator ==(Person& p) const { if (m_Age==p.m_Age && m_Name==p.m_Name) return true ; else return false ; } string m_Name; int m_Age; };void test01 () { Person p1 ("Tom" ,18 ) ; Person p2 ("Tom" ,18 ) ; if (p1==p2) cout<<"相等" <<endl; }
函数调用运算符重载 使用方式类似函数的调用,因此被称为仿函数
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 #include <iostream> #include <string> using namespace std;class MyPrint {public : void operator () (string test) { cout<<test<<endl; } };class MyAdd {public : int operator () (int num1, int num2) { return num1 + num2; } };void test01 () { MyPrint myPrint; myPrint ("Hello world" ); }void test02 () { MyAdd myAdd; int ret = myAdd (3 ,4 ); cout<<ret<<endl; cout<<MyAdd ()(100 ,100 )<<endl; }int main () {test01 ();test02 ();return 0 ;}
继承 (inheritance) 继承是面向对象三大特性之一
有些类与类之间存在着特殊的关系,例如下图中:
flowchart TB
classDef node fill:#a0eee115,stroke:#333;
动物---猫 & 狗
猫---加菲猫 & 布偶猫 & A & 波斯猫
狗---哈士奇 & 京巴 & B & 德国牧羊犬
A["……"]
B["……"]
下级成员除了拥有上一级的共性,还有自己的特性。
利用继承的技术,可以减少重复代码
继承的基本语法 class 派生类 : 继承方式 基类;
继承方式
graph TB
classDef node fill:#a0eee115,stroke:#333;
A["<pre align='left' class='cpp' style='font-family:Ubuntu Mono,monospace;'>class A
{
public:
int a;
protected:
int b;
private:
int c;
};</pre>"]--公共继承---B["<pre align='left' class='cpp' style='font-family:Ubuntu Mono,monospace;'>class B: public A
{
public:
int a;
protected:
int b;
//不可访问:
int c;
};</pre>"]
A--保护继承---C["<pre align='left' class='cpp' style='font-family:Ubuntu Mono,monospace;'>class B: protected A
{
protected:
int a;
int b;
//不可访问:
int c;
};</pre>"]
A--私有继承---D["<pre align='left' class='cpp' style='font-family:Ubuntu Mono,monospace;'>class B: private A
{
private:
int a;
int b;
//不可访问:
int c;
};</pre>"]
继承中的对象模型 父类中的所有非静态成员属性都会被子类继承下去
父类中的私有成员,被编译器隐藏,所以无法访问
继承中的构造和析构顺序 构造子类时:先调用父类构造函数,再调用子类构造函数
析构子类时:先调用子类析构函数,再调用父类析构函数
继承中的同名成员处理 问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
访问子类同名成员 直接访问即可 访问父类同名成员 需要加作用域 当子类与父类拥有同名的成员函数,子类会隐藏掉父类中的所有同名成员函数(包括重载的函数),加作用域可以访问到父类中的同名成员函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Base {public : Base (){m_A=100 ;} void func () {cout<<"Base-func调用" <<endl;} void func (int a) {cout<<"Base-func(int a)调用" <<endl;} int m_A; };class Son : public Base{public : Son (){m_A=200 ;} void func () {cout<<"Son-func调用" <<endl;} int m_A; };int main () { Son s; cout<<s.m_A<<endl; cout<<s.Base::m_A<<endl; s.func (); s.Base::func (); s.Base::func (0 ); return 0 ; }
同名静态成员的访问方式
通过对象访问
子类:s.m_A
父类:s.Base::m_A
通过类名访问
子类:Son::m_A
父类:Son::Base::m_A
多继承、菱形继承与虚继承 多 继 承 语 法
C++允许一个类继承多个类
语法:class 子类 : 继承方式 父类1, 继承方式 父类2...
多继承可能会出现父类中有同名成员的情况,子类使用时要加作用域
菱 形 继 承
flowchart TB
classDef node fill:#a0eee115,stroke:#333;
动物 --- 水生动物 & 陆生动物 --- 两栖动物
菱形继承的问题:
动物的数据经过菱形继承出现了两份,使用时会产生二义性 实际上只需要一份数据,产生了资源浪费 解决方法:虚继承
虚 继 承
关键字:virtual
1 2 3 4 5 6 7 8 9 class Base {public : int m_A; };class A : virtual public Base {}; class B : virtual public Base {};class AB : public A, public B {};
AB
从 A
, B
继承下来的是 vbptr
vbptr 即 virtual base pointer ,虚基类指针
graph TB
subgraph class AB
subgraph
A["(base class A)<br/>{vbptr}"]
B["(base class B)<br/>{vbptr}"]
end
Ba["(virtual base Base)<br/>m_A"]
end
虚继承时,虚基类指针指向虚基类表(vbtable),虚基类表中存放的是数据相对于虚基类指针的偏移,从而根据偏移找到数据
graph LR
A["{vbptr}"]
T["(vbtable)<br/>
0    |0         <br/>
1    |偏移量"]
M["m_A"]
A --> T --> M
多态 (polymorphism) 多态的基本概念 多态分为两类
静态多态:函数重载 和 运算符重载 属于静态多态,复用函数名 动态多态:派生类 和 虚函数实现运行时多态 静态多态和动态多态的区别:
静态多态:函数地址早绑定,编译阶段确定函数地址 动态多态:函数地址晚绑定,运行阶段确定函数地址 动态多态:使用虚函数 ,实现函数地址晚绑定
关键字:virtual
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 class Animal {public : virtual void speak () { cout<<"动物在说话" <<endl; } };class Cat : public Animal{public : void speak () { cout<<"小猫在说话" <<endl; } };class Dog : public Animal{public : void speak () { cout<<"小狗在说话" <<endl; } };void DoSpeak (Animal& animal) { animal.speak (); }void test01 () { Cat cat; DoSpeak (cat); Dog dog; DoSpeak (dog); }int main () {test01 ();return 0 ;}
多态满足条件:
有继承关系 子类重写(override)父类的虚函数 多态使用条件:
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
多态的原理 vfptr:virtual function pointer,虚函数(表)指针
vftable:virtual function table,虚函数表
当子类重写父类的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址
当父类的指针或引用指向子类对象的时候,发生多态
Animal& animal = cat;
animal.speak();
flowchart TB
subgraph s1["Animal 类内部结构"]
v2["vfptr"]
subgraph s11["vftable"]
v3["&Animal::speak"]
end
end
subgraph s2["Cat 类内部结构"]
v4["vfptr"]
subgraph s21["vftable"]
v5["&Animal::speak"]
end
end
subgraph s3["Cat 类内部结构"]
v6["vfptr"]
subgraph s31["vftable"]
v7["&Cat::speak"]
end
end
v2 --> s11
v4 --> s21
v6 --> s31
s1 -.继承.-> s2
s2 -.重写.-> s3
纯虚函数和抽象类 在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改成纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为==抽象类==
抽象类特点:
无法实例化对象 子类必须重写抽象类中的纯虚函数,否则也属于抽象类 虚析构和纯虚析构 多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
可以解决通过父类指针释放子类对象 都需要有具体的函数实现 虚析构和纯虚析构区别:
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
注:如果子类中没有堆区数据,可以不写为虚析构或纯虚析构