一.C++中的类概述
1.C++中的类(class)是面向对象编程(OOP)的核心概念之一,它提供了一种封装数据和方法以实现特定功能的机制。
2.C++类的原理
(1)封装:
类通过封装将相关的数据(成员变量)和操作这些数据的方法(成员函数)组合在一起,形成一个独立的实体。
封装隐藏了数据的具体实现细节,只对外公开必要的接口(即成员函数),从而提高了代码的安全性和可维护性。
C++通过访问说明符(如public、private、protected)来控制类成员的访问权限,进一步增强了封装性。
(2)继承:
继承是面向对象编程中的一个重要特性,它允许一个类(派生类)继承另一个类(基类)的属性和方法。
通过继承,可以实现代码的重用和扩展。派生类可以继承基类的所有public和protected成员,并可以在此基础上添加新的功能或修改已有的功能。
(3)多态:
多态性允许在派生类对象上执行基类中定义的操作,但可以有不同的行为。
这通常通过虚函数和动态绑定来实现。在C++中,当通过基类指针或引用来调用虚函数时,会根据对象的实际类型来调用相应的函数版本。
3.C++类的作用
(1)数据抽象:
类通过定义成员变量和成员函数来描述一个对象的属性和行为,实现了对现实世界中的抽象概念建模。
数据抽象使得代码更加直观、易于理解和组织。
(2)代码重用:
通过类的继承机制,可以实现代码的重用。派生类可以继承基类的属性和方法,避免了重复编写相同的代码。
(3)模块化:
一个类就是一个模块,它封装了相关的数据和方法。这使得我们可以很方便地将多个模块组装起来,完成目标功能。
在软件更新升级时,只要将类对象修改为最新版本即可,无需对每个方法的调用都进行修改。
(4)提高代码的可维护性:
封装使得代码更加模块化,降低了模块间的耦合度,提高了代码的可维护性。
当需要修改某个类的实现时,只需在该类内部进行修改,不会影响到其他模块。
(5)提高代码的安全性:
通过访问说明符控制类成员的访问权限,可以保护类的内部数据不被随意修改,从而提高了代码的安全性。
3.类的实现
(1)定义类
使用关键字class来定义类,后面跟着类名(遵循C++的命名规则,如使用驼峰命名法或下划线分隔)。
类定义中可以包含数据成员(即属性)和成员函数(即方法)。
成员可以是公有的(public)、私有的(private)或受保护的(protected),以控制访问权限。
(2)成员函数实现
成员函数可以在类定义时直接实现(称为内联函数),也可以在类定义外部实现。
在类外部实现成员函数时,需要使用作用域解析运算符::来指定该函数属于哪个类。
(3)构造函数和析构函数
构造函数是一种特殊的成员函数,用于在创建对象时初始化其成员变量。构造函数没有返回类型,且与类名相同。
析构函数也是特殊的成员函数,用于在对象销毁时执行清理工作。析构函数以波浪号~开头,且与类名相同,没有返回类型和参数。
4.可见性
(1).private
只能在该类的内部访问,子类也不行。只能提供类中的函数进行调用与修改,在其他地方不能使用
(2)protected
自己与子类(派生类)可以访问
(3)public
任何地方都可以访问。
5.构造函数和析构函数
构造函数:构造函数是一种特殊的成员函数,用于创建对象时进行初始化。它与类同名,没有返回类型,并且可以有参数。当我们创建一个对象时,构造函数被自动调用,用于初始化对象的数据成员。
析构函数:析构函数也是一种特殊的成员函数,用于在对象销毁时执行清理工作。它与类同名,前面加上波浪号(~),没有返回类型和参数。当对象超出作用域、程序结束或者使用delete操作符释放动态分配的对象时,析构函数被自动调用。
二.C++中类的基本应用
以下是一个简单的C++类示例,演示了类的定义、成员函数的实现以及对象的创建和使用:
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0) : xPos(x), yPos(y) { cout << "x = " << xPos << ", y = " << yPos << endl; } // 构造函数
void setPoint(int x, int y) { xPos = x; yPos = y; } // 设置点坐标
void printPoint() const { cout << "x = " << xPos << ", y = " << yPos << endl; } // 打印点坐标
int xPointOffset(int x);
~Point(); // 析构函数声明
private:
int xPos, yPos; // 点的坐标
};
Point::~Point() {
cout << "Point 析构函数被调用" << endl;
// 清理代码...
}
int Point::xPointOffset(int x)
{
xPos = x + 10;
return x + 10;
}
int main() {
Point p1; // 创建Point对象p1,使用默认构造函数
p1.setPoint(10, 20); // 设置p1的坐标
p1.printPoint(); // 打印p1的坐标
Point p2(30, 40); // 创建Point对象p2,并初始化坐标
p2.xPointOffset(60);
p2.printPoint(); // 打印p2的坐标
return 0;
}
输出:
x = 0, y = 0
x = 10, y = 20
x = 30, y = 40
x = 70, y = 40
Point 析构函数被调用 //p1释放
Point 析构函数被调用 //p2释放
三.虚函数 virtual及应用
1.用于实现多态,子类可以重写父类的虚函数。
(1)原理:利用了动态联编,通过虚函数表来在运行时确定调用的函数。
对于非虚函数,在编译时就确定要调用哪个函数,因此调用非函数
(2)虚函数表:
当一个类包含虚函数时,会生成虚函数表,保存虚函数的地址。其派生类也会有虚函数表
创建类实例时,若有虚函数,则会生成虚函数指针,指向其虚函数表。
当一个基类的指针指向了 子类对象,则运行时,会使用子类的虚函数表指针寻找子类虚函数的地址,而不是基类的。
(3)编译器创建虚函数表的过程:
拷贝每个基类的虚函数表。
查看子类是否重写了基类的虚函数,有则修改子类的虚函数表;查看子类是否增加了虚函数,有则加入虚函数表
(4)虚函数的缺点:
增大内存的消耗
每次调用虚函数时,要遍历虚函数表
2.虚函数使用情况:
有一个基类与一个子类
class Base(){
void Print1(){// 非虚函数
cout<<"Base"<<endl;
}
virtual void Print2(){
cout<<"Base"<<endl;
}
};
class Son():public Base{
void Print1(){
cout<<"Son"<<endl;
}
void Print2() override{// 重写虚函数,加上override关键字,非必要,但可以检测错误
cout<<"Son"<<endl;
}
};
(1).基类指针指向子类,且调用非虚函数:会调用基类的函数,而不是子类的函数
int main(){
Son* s=new Son();
Base* base=s;// 基类指针指向
base->Print1();// 输出“Base”,非虚函数
}
(2).基类指针指向子类,且调用虚函数:会根据虚函数表调用子类函数,
int main(){
Son* s=new Son();
Base* base=s;// 基类指针指向
base->Print2();// 输出“Son”,虚函数
}
3.纯虚函数(接口)
基类的虚函数只是一个声明,并没有实现。子类必须实现该虚函数。
使用场景:可以用于制作模板,规定子类都必须实现某些函数
有纯虚函数的子类,不能实例化。
class Base(){
virtual void Print()=0;// 纯虚函数
};
class Son1():public Base{
void Print() override{// 实现纯虚函数,加上override关键字,非必要,但可以检测错误
cout<<"Son1"<<endl;
}
};
class Son2():public Son2{
void Print() override{// 由于基类实现了纯虚函数,所以该类可以不重写Print()
cout<<"Son2"<<endl;
}
};
void PrintName(Base* b){
b->Print();
}
int main(){
Base* base=new Base();// 会报错,不能实例化
Son1* son1=new Son1();
Son2* son2=new Son2();
PrintName(son1);// 输出“Son1”
PrintName(son2);// 输出“Son2”
}
四.静态成员函数
1.静态成员函数的特点:
不依赖于对象实例:
静态成员函数只能访问静态成员变量或其他静态成员函数;不能访问类的非静态成员(包括非静态成员变量和非静态成员函数),因为它们不依赖于任何特定的对象实例。如果静态成员函数需要访问非静态成员,它必须接收一个指向该类实例的指针或引用作为参数。
属于类:
静态成员函数是类的一部分,而不是任何特定对象的一部分。这意味着,无论创建多少个类的实例,静态成员函数都只有一份拷贝,并且这些拷贝在所有的实例之间是共享的。因此,它们不能使用this指针(在C++中)或隐式的实例引用(在某些其他面向对象的语言中),因为this指针或隐式实例引用指向的是类的实例,而静态成员函数不依赖于任何实例。
通过类名调用:
静态成员函数通常通过类名加函数名的方式调用,而不是通过类的实例调用。这有助于区分静态成员函数和非静态成员函数。
用途:静态成员函数常用于实现那些与类本身相关的功能,而不是与类的特定实例相关的功能。例如,它们可以用于实现工具函数、工厂方法或执行仅依赖于输入参数而不需要类实例状态的操作。
2.代码示例
#include <iostream>
using namespace std;
class MyClass {
public:
static void staticFunction() {
cout << "这是一个静态成员函数" << endl;
}
void nonStaticFunction() {
cout << "这是一个非静态成员函数" << endl;
}
private:
int m_age; // 年龄
// 在类内部定义静态成员
static int number;
};
// 在类外部初始化静态成员变量
int Student::number = 1;
int main() {
// 通过类名调用静态成员函数
MyClass::staticFunction(); // 输出: 这是一个静态成员函数
// 尝试通过实例调用静态成员函数(虽然技术上可行,但通常不推荐这样做)
MyClass obj;
obj.staticFunction(); // 输出: 这是一个静态成员函数
// 调用非静态成员函数,必须通过实例
obj.nonStaticFunction(); // 输出: 这是一个非静态成员函数
return 0;
}
五.this指针的用途
访问对象的成员:当成员函数需要访问调用它的对象的成员时,可以使用this指针。尽管在大多数情况下,你可以直接访问成员而不需要显式地使用this指针,但在某些情况下(如参数名与成员名冲突时),使用this指针可以明确指定你正在访问的是对象的成员。
返回对象本身:在某些情况下,成员函数可能需要返回调用它的对象本身。这时,可以使用return *this;来实现链式调用(也称为级联调用)。
作为函数参数:虽然不常见,但在某些特殊情况下,你可能需要将this指针作为参数传递给其他函数,以便这些函数能够访问调用对象的成员。