目录
简介
C++ 是一种高级语言,它是由 Bjarne Stroustrup 于 1979 年在贝尔实验室开始设计开发的。C++ 进一步扩充和完善了 C 语言,是一种面向对象的程序设计语言。C++ 可运行于多种平台上,如 Windows、MAC 操作系统以及 UNIX 的各种版本。
本教程是专门为初学者打造的,帮助他们理解与 C++ 编程语言相关的基础到高级的概念。
封装
所有的 C++ 程序都有以下两个基本要素:
- 行为:这是程序中执行动作的部分,它们被称为函数。
- 属性:属性是程序的信息,会受到程序函数的影响。
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。默认情况下,在类中定义的所有项目都是私有的。
封装的写法并不唯一,在实际的过程中会结合业务需求而定,下面的以一个最基础的案例进行编写。通常先对属性私有化,使属性隐藏,然后根据当前属性的需求,通过getter函数和setter函数对类外分别公开读和写的功能。
#include <iostream>
using namespace std;
class MobilePhone
{
private: // 私有权限:只有类内部可访问
string brand; // 可读可写
string model; // 只写
int weight = 188; // 初始值
public: // 公开接口
string get_brand() // getter:读属性
{
return brand;
}
void set_brand(string b) // setter:写属性
{
brand = b;
}
void set_model(string m) // setter
{
model = m;
}
int get_weight() // getter
{
return weight;
}
};
int main()
{
MobilePhone mp1;
mp1.set_brand("小米");
mp1.set_model("13 Pro");
cout << mp1.get_brand() << " " << mp1.get_weight() << endl;
MobilePhone* mp2 = new MobilePhone;
mp2->set_brand("红米");
mp2->set_model("K60 Pro");
cout << mp2->get_brand() << " " << mp2->get_weight() << endl;
delete mp2;
return 0;
}
封装最主要的作用是提升程序安全性。
继承
1.1概念与基础使用
继承就是在一个已经存在的类的基础上建立一个新的类,并拥有其特性,体现了代码复用的思想。
已经存在类被称为“基类”或“父类”;
新建立的类被称为“派生类”或“子类”。
例如:
#include <iostream>
using namespace std;
/**
* @brief The Father class 基类
*/
class Father
{
public:
string first_name = "王";
void work()
{
cout << "我是个厨师" << endl;
}
};
/**
* @brief The Son class 派生类
*/
class Son:public Father
{
};
int main()
{
Son s;
cout << s.first_name << endl; // 王
s.work(); // 我是个厨师
return 0;
}
上面的代码中Son类与Father基本一致,在实际开发中,无需保持一致,通常派生类会在继承的基础上增加或修改一些基类的内容。
#include <iostream>
using namespace std;
/**
* @brief The Father class 基类
*/
class Father
{
public:
string first_name = "王";
void work()
{
cout << "我是个厨师" << endl;
}
};
/**
* @brief The Son class 派生类
*/
class Son:public Father
{
public:
Son()
{
// 如果派生类对基类的属性值不满意
// 可以修改,前提是能修改
first_name = "刘";
}
// 函数隐藏:对基类的函数不满意
void work()
{
cout << "我是个律师" << endl;
}
// 增加内容
void play()
{
cout << "我喜欢打游戏" << endl;
}
};
int main()
{
Son s;
cout << s.first_name << endl; // 刘
s.work(); // 我是个律师
s.play(); // 我喜欢打游戏
// 调用被隐藏的基类函数
s.Father::work(); // 我是个厨师
return 0;
}
需要注意的是,基类和派生类是相对的,某个类可能既是类A的基类,又是类B的派生类;派生类往往比基类更具体,基类往往比派生类更抽象。
1.2构造函数
类的构造函数和析构函数不能被继承。
#include <iostream>
using namespace std;
class Father
{
private:
string name;
public:
Father(string name):name(name){}
string get_name() const
{
return name;
}
void set_name(string name)
{
this->name = name;
}
};
class Son:public Father
{
public:
};
int main()
{
// Son s("张三"); 错误
return 0;
}
上面的例子可以看出,派生类并没有继承基类的带参数的构造函数。实际上1.1节中的派生类Son也没有继承Father的构造函数,这是因为当程序员不手写构造函数时,编译器会增加以下代码:
- 给基类Father增加一个无参构造函数
- 给派生类Son增加一个无参构造函数
- 在派生类Son的无参构造函数中,调用基类的无参构造函数
因为派生类中省略了基类的代码,因此在创建派生类对象时,需要调用基类的代码完成派生类中基类代码部分内存的开辟,即派生类的任意一个构造函数都必须直接或间接调用基类的任意一个的构造函数。
派生类调用基类构造函数的写法:
- 透传构造
- 委托构造
- 继承构造(只是叫这个名字,并没有继承构造函数)
1.2.1透传构造
透传构造指的是在派生类的构造函数中直接调用基类的构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name = "无名氏";
int age = 1;
public:
Father(string name)
:name(name)
{
cout << "Father的一参构造函数" << endl;
}
Father(string name,int age)
:name(name),age(age)
{
cout << "Father的二参构造函数" << endl;
}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
Son(string name)
:Father(name)
{
}
Son(string name,int age)
:Father(name,age)
{
}
};
int main()
{
Son s("张三",12);
s.show();
Son s2("李四");
s2.show();
return 0;
}
1.2.2委托构造
委托构造指的是,某个类的构造函数可以调用这个类的另一个重载的构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name = "无名氏";
int age = 1;
public:
Father(string name)
:name(name)
{
cout << "Father的一参构造函数" << endl;
}
Father(string name,int age)
:name(name),age(age)
{
cout << "Father的二参构造函数" << endl;
}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
Son(string name)
:Son(name,10)
{
}
Son(string name,int age)
:Father(name,age)
{
}
};
int main()
{
Son s("张三",12);
s.show();
Son s2("李四");
s2.show();
return 0;
}
委托构造需要注意以下几点:
- 如果是派生类,最终委托的构造函数要透传调用基类的构造函数。
- 不要形成委托闭环。
1.2.3继承构造
是C++11的新写法,在派生类中使用继承构造后,编译器会按照基类的构造函数格式,创建出派生类的构造函数,并且分别使用透传构造调用基类的同参数构造函数。
#include <iostream>
using namespace std;
class Father
{
private:
string name = "无名氏";
int age = 1;
public:
Father(string name)
:name(name)
{
cout << "Father的一参构造函数" << endl;
}
Father(string name,int age)
:name(name),age(age)
{
cout << "Father的二参构造函数" << endl;
}
void show()
{
cout << name << " " << age << endl;
}
};
class Son:public Father
{
public:
using Father::Father; // 继承构造
};
int main()
{
Son s("张三",12);
s.show();
Son s2("李四");
s2.show();
return 0;
}
实际开发不建议使用继承构造。
1.3 对象的创建与销毁
#include <iostream>
using namespace std;
/**
* @brief The Value class
* 作为其它类的变量
*/
class Value
{
private:
string name;
public:
Value(string name):name(name)
{
cout << name << "创建了" << endl;
}
~Value()
{
cout << name << "销毁了" << endl;
}
};
class Father
{
public:
static Value s_value;
Value value = Value("Father类的成员变量");
Father()
{
cout << "Father的构造函数" << endl;
}
~Father()
{
cout << "Father的析构函数" << endl;
}
};
Value Father::s_value = Value("Father类的静态成员变量");
class Son:public Father
{
public:
static Value s_value;
Value value = Value("Son类的成员变量");
Son()
{
cout << "Son的构造函数" << endl;
}
~Son()
{
cout << "Son的析构函数" << endl;
}
};
Value Son::s_value = Value("Son类的静态成员变量");
int main()
{
cout << "主函数开始执行" << endl;
{ // 局部代码块
Son s;
cout << "Son类对象使用中......" << endl;
}
cout << "主函数结束执行" << endl;
return 0;
}
上述运行结果可以发现遵循如下规律:
- 创建流程与销毁流程对称。
- 相同部分的创建,基类先派生类后;相同部分的销毁,派生类先基类后。
- 静态成员先创建,后销毁。
一个派生类对象的创建与销毁需要逐层调用到最上层的基类,因此体现了面向对象编程的特点:编写效率高,运行效率低。
可以使用继承,但是不要滥用继承。
1.4 多重继承
1.4.1 基础使用
之前的代码都是单继承,即一个派生类只有一个基类,实际上C++是支持多继承的,即一个派生类可以有多个基类,派生类在多重继承中与每一个基类的关系,仍然可以看做是一个单继承。
#include <iostream>
using namespace std;
class Sofa
{
public:
void sit()
{
cout << "能坐着" << endl;
}
};
class Bed
{
public:
void lay()
{
cout << "能躺着" << endl;
}
};
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
return 0;
}
1.4.2 二义性问题
多重继承容易出现二义性问题,主要有两种情况:
- 在多继承中,不同的基类拥有同名成员,此时派生类的调用会出现二义性问题。
解决方法:可以通过类名::的方式区分重名成员
#include <iostream>
using namespace std;
class Sofa
{
public:
void sit()
{
cout << "能坐着" << endl;
}
void position()
{
cout << "放在客厅" << endl;
}
};
class Bed
{
public:
void lay()
{
cout << "能躺着" << endl;
}
void position()
{
cout << "放在卧室" << endl;
}
};
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
// sb.position(); 错误
sb.Bed::position();
sb.Sofa::position();
return 0;
}
- 菱形(钻石)继承:如果一个基类有两个派生类,这两个派生类又作为基类拥有一个派生类,此时在最终的派生类中访问间接基类的成员会出现二义性。
解决方法1:可以通过类名::的方式区分重名成员
#include <iostream>
using namespace std;
class Furniture // 家具
{
public:
void func()
{
cout << "家里要有家具" << endl;
}
};
class Sofa:public Furniture
{
};
class Bed:public Furniture
{
};
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
// sb.func(); 错误
// sb.Furniture::func(); 错误
sb.Bed::func();
sb.Sofa::func();
return 0;
}
解决方法2:使用虚继承。
虚继承的实现通过虚基类指针与虚基类表完成,当Sofa和Bed使用虚继承继承Furniture类时,会在Sofa和Bed类中增加一个虚基类表,虚基类表中记录的是Furniture的成员,Sofa类(Bed类)的所有对象共用这一张虚基类表,通过每个对象创建时新增一个隐藏成员虚基类表指针调用虚基类表。SofaBed继承了Sofa和Bed类,SofaBed对象也会有继承来的虚基类指针,虚基类指针可以查询Sofa和Bed的虚基类表,在调用时通过查表来区分二义性问题。
#include <iostream>
using namespace std;
class Furniture // 家具
{
public:
void func()
{
cout << "家里要有家具" << endl;
}
};
class Sofa:virtual public Furniture
{
};
class Bed:virtual public Furniture
{
};
class SofaBed:public Sofa,public Bed
{
};
int main()
{
SofaBed sb;
sb.func();
return 0;
}
虚继承在代码形式上完美解决了菱形继承的二义性问题,但是在调用这些原本二义性的代码时,增加了一些开销(虚基类表与虚基类指针内存占用和查询等),因此虚继承代码执行效率比普通继承要低。
多态
1.1 概念
多态按照字面的意思可以认为是“多种状态”,可以简单概括为“一个接口,多种状态”,即程序在运行时动态决定调用的代码。
多态与模板的区别在于,模板针对不同的数据类型采用同样的策略,而多态针对不同的数据类型采用不同的策略。
多态的实现需要以下3个条件:
1. 要有公有继承
2. 要有函数覆盖:派生类中覆盖基类的成员函数
3. 基类引用/指针指向派生类对象
1.2函数覆盖
函数覆盖与函数隐藏类似,但是函数覆盖可以通过虚函数来支持多态,一个成员函数使用virtual关键字修饰,这个函数就是虚函数。
在派生类中,使用之前函数隐藏的方式重新实现一个基类中的虚函数,此时就是函数覆盖,函数覆盖与虚函数具有以下特点:
- 当函数覆盖成功时,虚函数具有传递性
- C++11中可以在派生类的新覆盖的函数后增加override关键字进行覆盖是否成功的验证
- 成员函数与析构函数可以定义为虚函数,静态成员函数与构造函数不可以定义为虚函数。
- 如果成员函数的声明与定义分离,virtual关键字只需要在声明时使用
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
};
int main()
{
Animal a;
a.eat(); // 吃东西
Dog d;
d.eat(); // 吃骨头
return 0;
}
1.3使用方式
多态既可以引用的方式,也可以使用指针的方式实现。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "吃鱼" << endl;
}
};
int main()
{
Dog d;
Cat c;
// 基类引用派生类对象
Animal& a1 = d;
Animal& a2 = c;
// 多态
a1.eat(); // 吃骨头
a2.eat(); // 吃鱼
Dog* d2 = new Dog;
Cat* c2 = new Cat;
// 基类指针指向派生类对象
Animal* a3 = d2;
Animal* a4 = c2;
// 多条
a3->eat(); // 吃骨头
a4->eat(); // 吃鱼
// 先别管delete问题
return 0;
}
1.4应用
多态的主要应用是函数的参数传递,从而实现“一个接口,多种状态”的效果。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "吃鱼" << endl;
}
};
// 基于引用的多态参数传递
void test_eat1(Animal& a)
{
a.eat();
}
// 基于指针的多态参数传递
void test_eat2(Animal* a)
{
a->eat();
}
int main()
{
Dog d1;
Cat c1;
Animal a1;
// 测试基于引用的多态参数传递
test_eat1(d1); // 吃骨头
test_eat1(c1); // 吃鱼
test_eat1(a1); // 吃东西
Dog* d2 = new Dog;
Cat* c2 = new Cat;
Animal* a2 = new Animal;
// 测试基于指针的多态参数传递
test_eat2(d2); // 吃骨头
test_eat2(c2); // 吃鱼
test_eat2(a2); // 吃东西
// 先别管delete问题
return 0;
}
1.5原理
当一个类中有虚函数时,编译器会为这个类创建一个虚函数表,这个类的对象拥有隐藏的成员变量虚函数表指针指向虚函数表。此类被继承时,虚函数表也会被继承,但是如果当前继承的派生类中出现函数覆盖,则会在派生类中更新这张表。
注:重写=覆盖
在实际多态的运行中是一个动态类型绑定的过程,当使用基类引用或指针指向派生类对象时,编译器会产生一段代码,用来检查当前内存中的对象的真正类型,在运行时通过对象的虚函数表指针找到真正的调用函数,因此多态也是一个查表的过程。
使用多态会产生一些额外的代码调用开销,不要滥用多态。
1.6缺陷
当形成多态时,有可能会出现内存泄露的问题。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
~Animal()
{
cout << "Animal析构函数" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
~Dog()
{
cout << "Dog析构函数" << endl;
}
};
int main()
{
Dog* d = new Dog;
delete d; // 先调用Dog的析构,再调用Animal的析构
Animal* a = new Dog;
delete a; // 只调用Animal的析构函数
return 0;
}
上面问题的出现是由于析构函数没有函数覆盖,实际上析构函数也无法覆盖,但是析构可以设置为虚函数,且无需覆盖就能解决上述问题。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat()
{
cout << "吃东西" << endl;
}
virtual ~Animal()
{
cout << "Animal析构函数" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "吃骨头" << endl;
}
~Dog() // 仍然具有虚函数的传递性
{
cout << "Dog析构函数" << endl;
}
};
int main()
{
Dog* d = new Dog;
delete d; // 先调用Dog的析构,再调用Animal的析构
Animal* a = new Dog;
delete a; // 先调用Dog的析构,再调用Animal的析构
return 0;
}
如果一个类可能作为基类,建议都手写为虚析构函数,以防未来可能出现的内存泄露问题。