目录
一、面向对象的编程思维
面向对象(简称 OOP) 是一种编程范式, 它将数据和操作数据的方法封装在一起, 形成一个对象。在面向对象编程中, 我们使用类来描述对象的属性和行为,通过实例化类来创建具体的对象。 面向对象的主要特点包括封装、继承和多态。
谈及面向对象不可避免会涉及面向过程,两者同为程序设计的两大方式,其编程思维又有什么样的区别呢,让我们通过餐厅的比喻来理解这两种编程思维的差异。
1. 面向过程编程(POP):
- 想象你正在经营一家传统的中餐馆。你需要亲自做所有的事情,比如炒菜、切菜、洗碗等。你需要一步一步地完成这些任务,并且需要记住每个任务的详细步骤。
- 在编程中,POP就像是你手动编写所有的代码,从最基础的操作开始,逐步构建出复杂的功能。你需要明确地定义每一步操作,以及这些操作之间的顺序和依赖关系。
2. 面向对象编程(OOP):
- 现在,让我们想象你正在经营一家现代化的快餐店。这家店有各种各样的员工,比如厨师、服务员、收银员等。每个人都有自己的职责,他们可以独立完成自己的工作。
- 在编程中,OOP就像是你创建了一系列的对象,每个对象都有自己的属性和方法。你可以创建一个厨师对象,让它负责炒菜;创建一个服务员对象,让它负责服务客人。这样,你就可以通过调用这些对象的方法来完成复杂的任务,而不需要关心这些方法是如何实现的。
对比上面两种方式,不难看出,对于面向过程而言,它采取的是一种自上而下的设计方法,分析出解决问题所需要的步骤,然后用函数把这些步骤一一实现。
而对于面向对象的编程思维而言,则采取的是一种自下而上的设计方式,面向对象往往从问题的一部分着手,分析问题包含哪些对象,将这些对象抽象成为类的形式,一点一点地构建整个程序。就像是工具箱,当我们需要解决某一问题时,拿出工具箱打开就能找到合适的工具(对象),我们不需要关心锤子是怎么造出来的,我们只需要注意什么工具能实现什么功能,但要注意的是,不同的“工具箱”一般封装着不同的功能,你不可能指望着餐厅会给你修轮胎吧?
二、类和对象
从上文我们不难发现面向对象编程的核心在于封装,在C++中,类的声明是定义一个数据类型的蓝图,它包括了类的名称和类的对象所包含的内容。 声明一个类需要使用关键字"class",然后是类的名称。接下来是一对花括号,花括号内包含了类的成员变量和成员函数。
类声明的一般形式为以下代码:
Class classname//类名
{
Access specifiers://访问修饰符:private/public/protected
Date menbers/variables;//变量
Member function()//方法
{}
};
而对象,则是类的实例。在现实生活中,我们可以将对象视为一类事物的具体实体或样本。例如,一部手机是一个对象,它的状态有:品牌、型号、价格;行为有:打电话、连接蓝牙、玩游戏等。在编程语言中,创建一个对象就是根据类来生成一个具体的实例。通过这种方式,我们可以根据类来创建具有相同属性和行为的多个对象。
总的来说,类是一种模板或者说蓝图,它定义了某类对象的共同属性和方法;而对象则是类的实例化结果,代表了具体的实体。这两者的关系可以这样理解:对象是类的实例,类是对象的模板。
需要注意的是,类是抽象的,不占用内存,而对象是具体的,占用内存。
2.1、对象创建
类提供了对象的蓝图,我们通过以下实例尝试去创建定义phone数据类型:
#include <string>
#include<iostream>
using namespace std;
class Phone {
public:
string brand;//品牌
string model;//型号
bool Bluetooth;//蓝牙
double Memory;//内存
void connectToBluetooth() //内联函数
{
cout << "Connecting to Bluetooth..." << endl;// 蓝牙连接
}
void Call(string contact);
};
void Phone::Call(string contact)
{
cout << "Call to: " << contact << endl;// 打电话
}
int main()
{
Phone phone1;//声明对象phone1,类型为Phone
Phone phone2; //声明对象phone2,类型为Phone
}
关键字public确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。 我们也可以指定其他访问属性,该部分稍后会讲解。
这里我们可以注意到一个类型可以创建多个对象,他们都拥有各自的属性brand,model和price,以及共有的成员函数connectToBluetooth,Call。
可以注意到的是我们创建成员函数运用了两种方式,在函数内部定义的函数称为内联函数,在函数外部定义的函数需要注意格式(返回类型 所属类::函数名(参数))
内存的分配
在定义对象时,C++都会在内存中为每一个对象分配空间,如上面phone1,内存会为它分配品牌(string类型)、型号(string类型)和价格(int类型);
Phone类有两个成员函数connectToBluetooth和Call,而phone1作为Phone类的具体成员,当然可以使用这两个成员函数;
但是需要注意的是,成员函数是被各个对象共用的,成员函数并不占据一个具体对象的内存空间,
2.2、访问对象的数据成员
上面我们创建了一个Phone类型并创建了两个对象,接下来我们尝试着访问它们内置的数据成员
#include<iostream>
#include <string>
using namespace std;
class Phone {
public:
string brand;//品牌
string model;//型号
bool Bluetooth;//蓝牙
double Memory;//内存
void connectToBluetooth() //内联函数
{
cout << "Connecting to Bluetooth..." << endl;// 蓝牙连接
}
void Call(string contact); //电话
};
void Phone::Call(string contact)
{
cout << "Call to: " <<contact<< endl;// 打电话
}
int main()
{
//访问数据
Phone phone1;//声明对象phone1,类型为Phone
Phone phone2; //声明对象phone2,类型为Phone
phone1.brand = "Huawei";
phone2.brand = "Mi";
//调用成员函数
phone1.connectToBluetooth();
phone2.Call("Mike");
//打印
cout<<"phone1的品牌是: "<<phone1.brand<<endl;
cout<<"phone2的品牌是: "<<phone2.brand<<endl;
return 0;
}
类的对象的公共数据成员可以直接使用访问运算符.来访问。
2.3、类访问修饰符
上述我们创建的Phone类型中,我们可以发现当访问修饰符是public时,公有成员在类的外部是可以访问,修改的,但现实中手机肯定不允许我们擅自在外部修改内存大小或者型号吧。编程中也有相同的概念去限制访问权。
public(公有成员) 允许成员在类的外部访问 protected(私有成员) 不允许成员在类的外部访问,除了类和友元函数 private(受保护成员) 仅允许成员在派生类(即子类)中访问
#include<iostream>
#include <string>
using namespace std;
class Phone {
private:
string brand;//品牌
string model;//型号
bool Bluetooth;//蓝牙
double Memory;//内存
public:
double getMemory(void)
{
return Memory;
}
};
int main()
{
Phone phone1;
double memory;//设置变量接收返回值
phone1.Memory = 256.0;//error:不能直接访问成员数据,避免值被更改
memory = phone1.getMemory();//使用成员函数访问成员数据
cout << "内存(Gb)为: " << memory << endl;
return 0;
}
这里我们可以注意到,虽然我们不能在类外部直接访问受保护成员,但是我们可以通过成员函数去访问,这是允许的。
2.4、构造函数和析构函数
2.4.1、构造函数
构造函数是一种特殊的成员函数,用于初始化类的对象。在C++中,构造函数的名称与类名相同,没有返回类型。构造函数可以有参数,也可以没有参数。当创建类的对象时,构造函数会自动被调用。下面我们利用构造函数对成员数据的值进行初始化:
#include<iostream>
#include <string>
using namespace std;
class Phone
{
public:
Phone(string brand, double memory);
void getInf();//访问成员数据信息
private:
string Brand;//品牌
double Memory;//内存
};
void Phone::getInf()
{
cout << "品牌: " << Brand << endl << "内存: " << Memory;
}
Phone::Phone(string brand, double memory)//带参的构造函数
{
Brand = brand;
Memory = memory;
}
int main()
{
Phone phone1("Huawei", 512.0);//初始化
phone1.getInf();
return 0;
}
一般默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。在创建对象时为对象赋初始值;
注意:类的成员函数会访问对象的成员数据,因此当我们调用成员函数时,成员数据会被使用并可能发生变化。
2.4.2、析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
#include <iostream>
using namespace std;
class Phone {
public:
Phone() {
cout << "Phone对象创建" << endl;
}
~Phone() {
cout << "Phone对象销毁" << endl;
}
};
int main() {
Phone phone1; // 创建对象
return 0;
}
析构函数可以用来做“清理善后”的工作,例如在建立对象时用new开辟了一片内存,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。
2.4.3、拷贝构造函数
拷贝构造函数是一种特殊的构造函数,用于创建一个对象的副本。从一个已经存在的对象中复制数据到新建的对象中。在 C++ 中,拷贝构造函数是一个特殊的构造函数,它接受一个同类型的对象作为参数,用于初始化新创建的对象。拷贝构造函数通常用于:
-
当定义一个新对象时,它的值需要与一个已存在的对象相同,可以使用拷贝构造函数来完成拷贝操作。
-
当调用函数时,需要将一个对象作为参数传递给函数,或者从函数中返回一个对象时,也可以使用拷贝构造函数来完成拷贝操作。
#include<iostream>
#include <string>
using namespace std;
class Phone
{
public:
Phone(double memory); //构造函数
Phone(const Phone& obj);//拷贝构造函数
~Phone(); //析构函数
void getInf();
private:
double* Memory;//内存
};
void Phone::getInf()
{
cout << "内存: " << *this->Memory << endl;
}
Phone::Phone(double memory)
{
cout << "调用构造函数并为指针 ptr 分配内存" << endl;
Memory = new double;
*Memory = memory;
}
Phone::Phone(const Phone& obj)
{
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
Memory = new double;
*Memory = *obj.Memory; // 拷贝值
}
Phone::~Phone()
{
cout << "Phone对象销毁" << endl;
delete Memory;
}
int main()
{
Phone phone1(512.0);
Phone phone2(phone1);
phone1.getInf();
phone2.getInf();
return 0;
}
需要注意的是,如果一个类没有显式地定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数来完成对象的拷贝操作。但是,如果类中包含指针或动态分配内存等资源,那么默认的拷贝构造函数可能会出现问题。此时,需要自己定义一个拷贝构造函数来完成特定的拷贝操作,以确保程序的正确性
2.5、this指针
在 C++ 中,this 指针是一个指向当前对象的指针。每个对象在被创建时都会自动地创建一个 this 指针。this 指针可以访问当前对象的所有成员变量和成员函数。
#include <iostream>
using namespace std;
class Phone {
private:
double memory;
public:
void setMemory(double memory) {
this->memory = memory;
}
void printMemory() {
cout << "Memory: " << this->memory << endl;
}
};
int main() {
Phone phone1;
phone1.setMemory(256.0);
phone1.printMemory();
return 0;
}
在成员函数中使用 this 指针的主要目的是区分对象的成员变量和局部变量或参数变量之间的命名冲突。当对象的成员变量与局部变量或参数变量具有同名时,使用 this 指针可以准确地引用对象的成员变量
2.6、指向类的指针
一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。与所有的指针一样,您必须在使用指针之前,对指针进行初始化。
#include <iostream>
using namespace std;
class Phone {
private:
double memory;
public:
void setMemory(double memory) {
this->memory = memory;
}
void printMemory() {
cout << "Memory: " << this->memory << endl;
}
};
int main() {
Phone phone1;
phone1.setMemory(256.0);
Phone* ptr;
ptr = &phone1;
ptr->printMemory();
return 0;
}
2.7、类的静态成员
在C++中,我们可以使用 static 关键字来把类成员定义为静态的,静态成员属于类本身,而不是属于对象。静态成员在程序执行期间只有一个实例,不管创建了多少个对象。静态成员可以被所有类的对象共享,可以被类名直接访问,也可以被对象名访问。
静态成员变量必须在类外部进行定义,以便给它一个实际的存储空间。
#include <iostream>
using namespace std;
class Phone
{
public:
static int count;
Phone() {
count++;
}
};
int Phone::count = 0;
int main() {
Phone p1;
Phone p2;
Phone p3;
cout << "当前手机数量:" << Phone::count << endl;
return 0;
}
上面的代码定义了一个静态成员变量count,初值为0,通过Phone::count或对象名.count可以访问这个静态成员变量。
2.7.2、静态成员函数
如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。
#include <iostream>
using namespace std;
class Phone
{
public:
static int count;
Phone() {
count++;
}
static int getCount() {
return count;
}
};
int Phone::count = 0;
int main() {
Phone p1;
Phone p2;
Phone p3;
cout << "当前手机数量:" << Phone::getCount()<< endl;
return 0;
}
上面的代码定义了一个静态成员函数getCount,通过Phone::getCount或对象名.getCount可以访问这个静态成员函数。
静态成员的应用场景:
-
计数器:可以将静态成员变量定义为计数器,在每次创建对象时自增,记录类的实例个数。
-
单例模式:使用一个静态成员变量记录唯一的实例指针,在每次创建对象时判断该实例指针是否为NULL,如果为NULL则创建,否则直接返回实例指针。
2.8、重载函数
C++中的函数重载是指在同一作用域中定义多个函数名相同但参数列表不同的函数,以便根据不同的参数类型和数量对函数进行调用,例如:
int add(int a, int b){ return a + b; } float add(float a, float b){ return a + b; }
编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。
三、继承
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
在现实生活中我们可以发现周围的许多事物都有继承的影子,例如猫与狗的基类都是哺乳动物,且他们都继承了哺乳动物的特性:胎生等,但他们在继承哺乳动物的一些共同特征的同时,他们也有各自的特性,当我们用面向对象编程的思维去模仿现实世界时,可以使我们的代码更加灵活适应不同的需求。
接下来以我们的手机为例,我们都知道手机的型号在不断的更新换代,但是它们并抛弃手机的基础功能,而是建立在这个基础上去不断更新功能,我们简单地用代码描述:
#include <iostream>
using namespace std;
class Phone {
public:
void makeCall() {
cout << "正在打电话..." << endl;
}
void sendMessage() {
cout << "正在发短信..." << endl;
}
};
class SmartPhone : public Phone {
public:
void call_by_5g() {
cout << "5g通话中..." << endl;
}
void playGame() {
cout << "正在玩游戏..." << endl;
}
};
int main() {
SmartPhone mySmartPhone;
mySmartPhone.makeCall();
mySmartPhone.sendMessage();
mySmartPhone.playGame();
mySmartPhone.call_by_5g();
return 0;
}
需要注意的是一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数
3.2、基类初始化
如果基类包含重载的构造函数,需要在实例化时给它提供实参,该如何办呢?创建派生对象时将如何实例化这样的基类?方法是使用初始化列表,并通过派生类的构造函数调用合适的基类构造函数,如下面的代码所示:
#include <iostream>
using namespace std;
class Phone {
private:
bool isAndroidPhone;//设置一个布尔成员变量用来判断
public:
Phone(bool isAndroid):isAndroidPhone(isAndroid)//在构造函数中初始化isAndroidPhone
{
if (isAndroidPhone) {
cout << "这是一个安卓手机" << endl;
}
else {
cout << "这是一个非安卓手机" << endl;
}
}
};
class AndroidPhone : public Phone
{
public:
AndroidPhone() : Phone(true) {}
};
class NonAndroidPhone : public Phone
{
public:
NonAndroidPhone() : Phone(false) {}
};
int main()
{
AndroidPhone androidPhone;
NonAndroidPhone nonAndroidPhone;
return 0;
}
派生类没有直接访问布尔成员变量 Phone::isAndroidPhone,注意这个变量是通过Phone的构造函数设置的。为了最大限度地提高安全性,对于派生类不需要访问的基类属性,别忘了将其声明为私有的。
3.3、访问控制和继承
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员
派生类(行)
基类(列)
public protected private public √ √ √ protected √ √ √ private × × ×
四、多态/抽象类
多态是C++中的一个重要概念。它是面向对象程序设计的基础之一,也是C++封装和继承的补充。抽象类则是一种特殊的类,它不能被实例化,通常用来作为基类,定义一系列子类必须实现的方法。
多态性指同一类型的对象在不同状态下具有不同的行为。在C++中,多态性主要体现在两个方面:函数重载(2.8节)和虚函数。先举个简单例子:
#include <iostream>
using namespace std;
class Animal {
public:
void makeSound(){
cout << "动物发出声音" << endl;
}
};
class Dog : public Animal {
public:
void makeSound(){
cout << "狗汪汪叫" << endl;
}
};
class Cat : public Animal {
public:
void makeSound(){
cout << "猫喵喵叫" << endl;
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound();
animal2->makeSound();
delete animal1;
delete animal2;
return 0;
}
上诉代码正常设想下是根据动物种类输出相应的叫声,但是很抱歉并不是。调用函数 makeSound() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接,在编译阶段就确定了函数调用的具体实现,而不需要等到运行时才确定。
接下来我们尝试将makeSound()修改成虚函数。
class Animal {
public:
virtual void makeSound() {
cout << "动物发出声音" << endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
cout << "狗汪汪叫" << endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
cout << "猫喵喵叫" << endl;
}
};
irtual
关键字用于声明虚函数。虚函数是一种特殊的成员函数,它可以在基类中被声明为虚函数,然后在派生类中被重写。当通过基类指针或引用调用虚函数时,会执行实际对象的派生类中的版本,这就是多态的实现方式之一。
override
关键字用于表示一个成员函数是重写了基类中的虚函数。当子类的成员函数与基类的虚函数具有相同的名称、参数类型和返回类型时,可以使用override
关键字来显式地告诉编译器这个成员函数确实是重写了基类中的虚函数。
4.2虚函数
- 虚函数是一个被声明为virtual的成员函数。它的特殊之处在于,当一个类被继承时,如果子类中实现了一个与父类中的虚函数具有相同名称、参数列表和返回类型的函数,那么这个函数会自动成为虚函数。
- 虚函数可以被子类重写,也称为“覆盖”,当基类指针或引用指向一个子类对象时,调用虚函数时会动态地选择调用哪个函数,以实现多态性。这种操作被称为动态链接,或后期绑定。
- 使用虚函数的主要目的是在继承和多态性方面提供更大的灵活性。
4.3、纯虚函数
纯虚函数(Pure Virtual Function)是在基类中声明的虚函数,它没有任何定义,即没有函数体,只是为了让派生类实现,你可以把它理解为一个标准。
class Animal {
public:
virtual void makeSound() = 0;
};
纯虚函数(Pure Virtual Function)是在基类中声明的虚函数,它没有任何定义,即没有函数体,只是为了让派生类实现,你可以把它理解为一个标准。
在函数声明后面加上 =0
表示这个函数是一个纯虚函数。派生类必须实现该函数,否则派生类也将成为抽象类,无法实例化对象。
纯虚函数在多态性中非常有用,它可以实现基类的某些行为,但是没有具体的实现,将实现留给派生类。这样就可以在派生类中实现不同的行为,从而提高代码的可扩展性和复用性。
——————————————————END————————————————————