面向对象的三大特性为:封装、继承、多态

类的基本思想:数据抽象和封装

封装 (encapsulation)

将接口和实现分离

  • 权限控制

    • public 类内可访问,类外可访问,子类可访问
    • protected 类内可访问,类外不可访问,子类可访问
    • private 类内可访问,类外不可访问,子类不可访问
  • struct 和 class 的区别

    struct 的默认权限是 public

    class 的默认权限是 private

对象的初始化和清理

构造函数和析构函数

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,当类的对象被创建时,就会执行构造函数
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行清理工作

构造函数语法:类名(){}

  1. 函数名称和类名相同,无返回值
  2. 构造函数可以有参数,可重载

析构函数语法:~类名(){}

  1. 函数名称和类名相同,在前面加上~
  2. 析构函数不可以有参数,不能发生重载
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()
{
//1.括号法
Person p1; //无参构造函数调用
Person p2(10); //有参构造函数调用
Person p3(p2); //复制构造函数调用
//2.显示法
Person p4;
Person p5 = Person(10); //Person(10): 匿名对象,当前语句执行结束后会回收匿名对象
Person p6 = Person(p5);
//3.隐式转换法
Person p7 = 10;
Person p8 = p7; // 注意此处不会调用 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
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;
};

//1.使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
Person p1(20);
Person p2(p1);
}

//2.值传递的方式给函数参数传值
void doWork(Person p){}

void test02()
{
Person p;
doWork(p);
}

//3.以值方式返回局部对象
Person doWork2()
{
Person p1;
return p1;
}
void test03()
{
Person p = doWork2();
}

构造函数调用规则

默认情况下,C++编译器至少给一个类添加4个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认复制构造函数,对所有属性值进行复制
  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;

//shallow copy
//m_Height = p.m_Height;
//shallow copy仅仅复制了m_Height这个指针,而没有复制堆区的空间,会导致堆区的重复delete

//deep copy
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; //输出100

Person p2;
p2.m_A = 200;

cout<<p1.m_A<<endl; //输出200,所有对象共享
}

void test02(){
//静态成员变量不属于某个对象,所有对象都共享同一份数据
//因此静态成员变量有两种访问方式

//1.通过对象进行访问
Person p;
cout<<p.m_A<<endl;

//2.通过类名进行访问
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()
{
//1.通过对象访问
Person p;
p.func();

//2.通过类名访问
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){
//1.解决命名重复
this->num=num;
}

Apple& myAdd(Apple& p){
num+=p.num;
//2.返回本体
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成为常函数

    const实质上修饰的是this指针

  • 常函数内不可以修改成员属性

  • 成员属性声明时前面加关键字mutable,在常函数中依然可以修改

    mutable int m_B;

常对象:

  • 声明对象前加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.operator+(b2);或者Building b3 = operator+(b1, b2);简化为:
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;
}

//通常不会利用成员函数重载<<,因为成员函数重载只能实现 p<<cout,而不是 cout<<p

private:
int m_A;
int m_B;
};

//只能利用全局函数重载左移运算符,并返回cout,实现连续输出
ostream& operator<<(ostream& cout, Person& p) //本质 operator<<(cout,p) 简化为 cout<<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; }

//重载前置递增运算符,前置递增运算符可以连用(++(++a))
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个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认复制构造函数,对所有属性值进行复制
  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){
//m_Age = p.m_Age; //shallowCopy

//应该先判断是否有属性在堆区,如果有,先释放,再deepCopy
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)

继承是面向对象三大特性之一

有些类与类之间存在着特殊的关系,例如下图中:

下级成员除了拥有上一级的共性,还有自己的特性。

利用继承的技术,可以减少重复代码

继承的基本语法

class 派生类 : 继承方式 基类;

继承方式

  • public
  • protected
  • private

继承中的对象模型

父类中的所有非静态成员属性都会被子类继承下去

父类中的私有成员,被编译器隐藏,所以无法访问

继承中的构造和析构顺序

构造子类时:先调用父类构造函数,再调用子类构造函数

析构子类时:先调用子类析构函数,再调用父类析构函数

继承中的同名成员处理

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域
  • 当子类与父类拥有同名的成员函数,子类会隐藏掉父类中的所有同名成员函数(包括重载的函数),加作用域可以访问到父类中的同名成员函数
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;
}
  • 同名静态成员的访问方式

    1. 通过对象访问

      子类:s.m_A

      父类:s.Base::m_A

    2. 通过类名访问

      子类:Son::m_A

      父类:Son::Base::m_A

多继承、菱形继承与虚继承

多 继 承 语 法

C++允许一个类继承多个类

语法:class 子类 : 继承方式 父类1, 继承方式 父类2...

多继承可能会出现父类中有同名成员的情况,子类使用时要加作用域

菱 形 继 承

菱形继承的问题:

  1. 动物的数据经过菱形继承出现了两份,使用时会产生二义性
  2. 实际上只需要一份数据,产生了资源浪费

解决方法:虚继承

虚 继 承

关键字: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 {};

ABA , B 继承下来的是 vbptr

vbptr 即 virtual base pointer ,虚基类指针

虚继承时,虚基类指针指向虚基类表(vbtable),虚基类表中存放的是数据相对于虚基类指针的偏移,从而根据偏移找到数据

多态 (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;}

多态满足条件:

  1. 有继承关系
  2. 子类重写(override)父类的虚函数

多态使用条件:

  • 父类指针或引用指向子类对象

重写:函数返回值类型 函数名 参数列表 完全一致称为重写

多态的原理

vfptr:virtual function pointer,虚函数(表)指针

vftable:virtual function table,虚函数表

当子类重写父类的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址

当父类的指针或引用指向子类对象的时候,发生多态

Animal& animal = cat;

animal.speak();

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改成纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;

当类中有了纯虚函数,这个类也称为==抽象类==

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  1. 可以解决通过父类指针释放子类对象
  2. 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,则该类属于抽象类,无法实例化对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}

注:如果子类中没有堆区数据,可以不写为虚析构或纯虚析构