一、核心思想
面向对象编程的核心思想是将现实世界中的实体抽象为对象,每个对象都有自己的状态(属性)和行为(方法)。对象通过定义类来创建,类是对象的蓝图或模板,描述了对象的属性和方法。
这里的“对象”实际上是对现实世界中所存在的事物的一种抽象,举个例子,你在计算机世界中怎么表示“人”这个概念呢?
我们可以将之抽象为一个类Person, 并具有一些“属性”和“方法”。
1.1 属性
“属性”表示Person类所具有的特征,比如姓名、年龄、性别,通过这些特征,我们可以描述一个“人”的基本状态。
1.2 方法
“方法”表示Person类的行为和功能,比如吃饭、睡觉、行走,通过这些动作,我们可以描述一个“人”的动态行为。
比如下面的“伪代码”表示Person类的定义, 它包括姓名、性别、年龄和吃饭的方法。
Person {
// 姓名、性别、年龄等属性
name;
gender;
age;
// 吃饭的方法
eat() {
}
// 行走的方法
walk() {
}
}
1.3 类的写法
我们先了解一下类的基本写法:
class 类名{
访问修饰符:
// 成员变量,表示类的属性, 定义方式和变量的定义一样
// 成员方法,表示类的行为, 定义方式和方法的定义一样
}; // 分号结束一个类
C++使用class定义一个类,并在类中定义成员变量和成员方法。
1.4 访问修饰符
访问修饰符指定了成员变量和成员方法的可见性和访问权限。常用的修饰符包括 private(私有)、public(公有)和protected(受保护),
- public: 被修饰的成员在类的内部、派生类(子类)的内部和类的对象外部都可以访问。
- private: 被修饰的成员只能在定义该成员的类的内部访问。
- protected:被修饰的成员只能在定义该成员的类的内部以及派生类汇总访问。
二、C++前提知识
2.1 引用
引用 不是新定义一个变量,而是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
2.1.1 引用的表示方法
类型 & 引用变量名 ( 对象名 ) = 引用实体
引用实例
int main()
{
//引用:取别名
int a = 10;
int& b = a;//定义引用类型
int& c = b;
return 0;
}
引用特性
引用有三个特性,分别是:
- 引用在 定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
2.1.2 C++ 把引用作为函数入口参数
#include <iostream>
using namespace std;
// 函数声明
void swap(int& x, int& y);
int main ()
{
// 局部变量声明
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl;
cout << "交换前,b 的值:" << b << endl;
/* 调用函数来交换值 */
swap(a, b);
cout << "交换后,a 的值:" << a << endl;
cout << "交换后,b 的值:" << b << endl;
return 0;
}
// 函数定义
void swap(int& x, int& y)
{
int temp;
temp = x; /* 保存地址 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 x 赋值给 y */
return;
}
2.1.3 C++左值右值
C++ 增加了一个新的类型,右值引用,记作“&&”
-
左值
是指在内存中有明确的地址,我们可以找到这块地址的数据(可取地址)
-
右值
只提供数据,无法找到地址(不可取地址)
所有有名字的都是左值,而右值是匿名的
一般情况下,位于等号左边的是左值,位于等号右边的是右值,但是也可以出现左值给左值赋值的情况。
对于右值引用,看下面例子:
class Person {
private:
int age;
string name;
public:
// 默认构造函数
Person() {
age = 20;
name = "Tom"
}
// 带参数的构造函数
Person(int personAge, const string& personName) {
age = personAge;
name = personName
}
}
int main() {
// 使用默认构造函数创建对象
Person person1;
// 使用带参数的构造函数创建对象
Person person2(20, "Jerry");
return 0;
}
个人理解
Person(int personAge, const string& personName)
以及main中 Person person2(20, “Jerry”);
就相当于
string name = “Jerry”;
const string& personName =name;
但是写在构造函数中就相当于personName对字符串字面值“Jerry”(右值)的引用
为什么要加const?
string& personName是左值引用。
将字符串字面值传递给了一个非常量左值引用类型的参数。在这种情况下,C++ 编译器无法将字符串字面值直接绑定到非常量左值引用,因为字符串字面值是右值,不能直接绑定到非常量引用。
要解决这个问题,有三种方式:
1.将构造函数中的参数类型修改为 const 左值引用类型。这样就可以接受字符串字面值作为参数。
Person(int personage, const string& personname): age(personage), name(personname) {}
2.将构造函数中的参数类型修改为值传递(pass by value)的方式。这样可以接受任意类型的参数,包括字符串字面值。
Person(int personage, string personname): age(personage), name(personname) {}
3.右值引用&&
Person(int personage, string&& personname): age(personage), name(personname) {}
2.1.3.1 左值引用绑定特性
在 C++ 中,有两种引用类型:左值引用和右值引用。左值引用主要用于绑定左值,而右值引用主要用于绑定临时对象或右值。右值是指那些没有名称的、一次性的值,例如临时变量或表达式的结果。
当使用 const 左值引用来声明参数时,编译器会允许将右值(临时对象或右值表达式)绑定到该引用上,并将其视为常量引用。因此该参数不可修改。由于右值在绑定到 const 左值引用后也被视为常量,因此可以将右值绑定到该引用上。
2.2 构造函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时自动调用执行。构造函数用于初始化对象的成员变量,构造函数与类同名,没有返回类型。其基本语法包括:
- 函数名:与类名相同
- 参数列表:可以有零个或多个参数,用于在创建对象时传递初始化信息。
- 函数体: 用于执行构造函数的初始化逻辑。
2.2.1 使用初始化列表来初始化字段
使用初始化列表来初始化字段:
Line::Line( double len): length(len)
{
cout << "Object is being created, length = " << len << endl;
}
上面的语法等同于如下语法:
Line::Line( double len)
{
length = len;
cout << "Object is being created, length = " << len << endl;
}
三、C++三大特性
C++ 作为一种面向对象的编程语言,有着以下三大特性:封装、继承、多态
3.1 封装
- 封装(Encapsulation):
C++ 使用类来实现封装,将数据和操作封装在一起,使得对象具有更好的安全性和可维护性。在类中,可以将数据成员设置为私有的,只能通过公有的成员函数进行访问和修改。
我们假设有一个Circle类,它具有半径这个属性。然后我们创建一个圆对象
#include <iostream>
using namespace std;
class Circle{
public:
int radius;
};
int main(){
Circle circe;
circe.radius =10;
printf("半径为:%d,",circe.radius);
return 0;
}
创建对象之后,外部代码可以直接访问和修改半径,甚至将其设置为负数,这样的设计显然是不合理的。
为了防止这些问题的发生,我们可以通过封装隐藏对象中一些不希望被外部所访问到的属性或方法,具体怎么做呢?可以分为两步:
将对象的属性名,设置为private,只能被类所访问。
提供公共的get和set方法来获取和设置对象的属性。
class Circle {
// 私有属性和方法
private:
int radius; // 将圆的半径设置为私有的
// 公有属性和方法
public:
// setXX方法设置属性
void setRadius(int r) {
// 对输入的半径进行验证,只有半径大于0,才进行处理
if (r >= 0) {
radius = r;
} else {
cout << "半径不能为负数" << endl;
}
}
// getXXX方法获取属性
int getRadius() {
return radius;
}
};
使用封装,我们隐藏了类的一些属性,具体的做法是使用get方法获取属性,使用set方法设置属性,如果希望属性是只读的,则可以直接去掉set方法,如果希望属性不能被外部访问,则可以直接去掉get方法。
3.2 继承
-
继承(Inheritance):
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。 -
C++支持基于已有类创建新类的继承机制,通过继承,可以从父类获得属性和方法,同时也可以添加新的属性和方法。继承提高了代码的可重用性和扩展性。
-
实例:在对象中,总有一些操作是重复的,比如说Person类具有姓名、身高、年龄等特征,并具有一些行走、吃饭、睡觉的方法,而我们要实现一个Teacher类,Teacher首先也是一个人,他也具备人的特征和方法,那我们是不是也应该用代码去实现这些特征和方法呢,这就势必会产生一些重复的代码。
因此,我们可以采用“继承”的方式使得一个类获取到其他类中的属性和方法。在定义类时,可以在类名后指定当前类的父类(超类),
假设有一个基类 Shape,Rectangle 是它的派生类,如下所示:
#include <iostream>
using namespace std;
// 基类
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
return 0;
}
3.2.1 继承类型
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
-
公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
-
保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
-
私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
3.2.2 访问控制和继承
派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
我们可以根据访问权限总结出不同的访问类型,如下所示:
3.3 多态
- 多态(Polymorphism):多态常常和继承紧密相连,它允许不同的对象使用相同的接口进行操作,但在运行时表现出不同的行为。多态性使得可以使用基类类型的指针或引用来引用派生类的对象,从而在运行时选择调用相应的派生类方法。
C++中实现多态性的方法是通过virtual虚函数,下面的实例中,基类 Shape 被派生为两个类,如下所示:
#include <iostream>
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area();
return 0;
}
这段函数首先创建了一个基类 Shape 的指针 shape。然后创建了一个矩形对象 rec,并将其地址赋给 shape 指针,这样 shape 就指向了一个矩形对象。
接着通过 shape->area() 调用了通过基类指针访问的虚函数 area(),由于 shape 指向的是一个矩形对象,因此会调用矩形类中重写的 area() 函数。
然后创建了一个三角形对象 tri,同样将其地址赋给 shape 指针,这样 shape 就指向了一个三角形对象。
再次通过 shape->area() 调用虚函数 area(),由于 shape 指向的是一个三角形对象,因此会调用三角形类中重写的 area() 函数。
这样,在运行时根据实际对象的类型确定调用哪个类的虚函数,从而实现了多态性。
在 C++ 中,使用指针访问成员变量或成员函数可以通过两种方式实现:
-
使用点运算符(.):
如果是指向对象的指针,通过解引用指针再使用点运算符来访问成员变量或成员函数。例如:(*shape).area()。
如果是对象本身而不是指针,直接使用点运算符访问成员变量或成员函数。例如:rec.area()。 -
使用箭头运算符(->):
如果是指向对象的指针,直接使用箭头运算符访问成员变量或成员函数。例如:shape->area()。
这两种方式本质上是等价的,但使用箭头运算符可以简化代码,使其更加简洁和易读。所以,使用箭头运算符是比较常见的做法。
3.3.1 什么是指向对象的指针
指向对象的指针是指一个指针变量,它存储了一个对象的内存地址。通过这个指针,我们可以间接地访问和操作该对象。
在 C++ 中,可以使用指针来引用或指向一个对象。指针变量存储了对象在内存中的地址,通过解引用指针,我们可以访问指针所指向的对象的成员变量和成员函数。
例如,假设我们有一个类 MyClass,我们可以创建一个指向 MyClass 对象的指针:
MyClass obj; // 创建一个 MyClass 对象
MyClass* ptr = &obj; // 创建指向 MyClass 对象的指针,将其指向 obj 的地址
在这个例子中,ptr 是一个指向 MyClass 对象的指针,它存储了 obj 对象的地址。通过 ptr,我们可以访问 obj 对象的成员变量和成员函数,例如:ptr->memberVariable 或 ptr->memberFunction()。
指向对象的指针在动态内存分配、函数参数传递、类的继承和多态等场景中非常常见。通过指针,我们可以在程序运行时动态地操作和管理对象,实现更灵活和动态的代码行为。