C++类与对象学习笔记:封装、继承与多态
简介:C++编程语言以类和对象为核心,支持面向对象编程。本文深入介绍类的定义、对象的创建、构造函数和析构函数的作用,以及访问控制修饰符、继承和多态等面向对象编程的重要概念。通过具体示例,本文将帮助读者理解如何在C++中通过类和对象实现封装、继承和多态,从而编写出结构化、模块化的代码。 
1. C++类的定义和对象的创建
1.1 类的基本概念
在C++中,类是一种用户定义的数据类型,它允许将数据和操作数据的方法捆绑在一起。简而言之,类可以定义为一个模板,它描述了具有相同属性和行为的对象的集合。每个对象都是类的一个实例,它有自己的状态(通过成员变量)和行为(通过成员函数)。
1.2 类的定义方式
类的定义以关键字 class 开始,后跟类名和一对花括号,类体在花括号内定义。成员函数可以定义在类体内,也可以定义在类体外。成员变量定义了类对象的属性,成员函数则定义了类对象可以执行的操作。
class MyClass {
private:
int myVar; // 私有成员变量
public:
void setVar(int value) {
myVar = value; // 公有成员函数
}
};
1.3 对象的创建和使用
对象是类的实例,通过类定义来创建。对象的创建和使用涉及到类的构造函数和析构函数。构造函数在对象创建时初始化对象,而析构函数则在对象生命周期结束时清理资源。对象可以通过直接声明的方式创建,也可以通过动态分配和释放的方式创建。
MyClass obj; // 在栈上直接声明对象
obj.setVar(10); // 使用对象的方法
在下一章中,我们将深入探讨构造函数与析构函数的作用,了解它们如何在类的生命周期中发挥作用。
2. 构造函数与析构函数的作用
在C++面向对象编程中,构造函数和析构函数是两个非常重要的特殊成员函数。它们分别用于创建和销毁对象,并在对象的生命周期内发挥关键作用。理解构造函数和析构函数的定义、分类、作用和注意事项,对于编写出高效且无错的代码至关重要。
2.1 构造函数的定义和分类
构造函数是一种特殊的成员函数,其名称与类名相同,无返回类型,也不允许有返回值。它主要负责初始化对象,分配内存空间,并执行其他必要的初始化操作。
2.1.1 默认构造函数和参数化构造函数
默认构造函数是在没有提供任何实参的情况下,编译器自动生成的构造函数。它不带任何参数,用于创建对象时不做任何特殊初始化。
class MyClass {
public:
MyClass() {
// 默认构造函数的实现
}
};
参数化构造函数允许程序员定义构造对象时所需提供的参数。这种构造函数用于在创建对象时初始化对象的状态。
class MyClass {
public:
MyClass(int value) {
// 参数化构造函数的实现
}
};
2.1.2 拷贝构造函数的作用及实现
拷贝构造函数是一个特殊的构造函数,它的作用是用一个已存在的对象来创建一个新的对象。拷贝构造函数的一个重要特点是能够保证对象的深拷贝,特别是当对象中包含指向动态分配内存的指针时。
class MyClass {
private:
int* data;
public:
MyClass(const MyClass& other) {
data = new int;
*data = *(other.data); // 深拷贝
}
};
2.2 析构函数的角色和重要性
析构函数也是C++中的一个特殊成员函数,用于在对象生命周期结束时进行清理工作。析构函数在销毁对象之前被自动调用,释放对象使用的资源。
2.2.1 何时会调用析构函数
析构函数会在以下几种情况下被调用:
1. 对象生命周期结束时,如局部对象在函数结束时。
2. delete运算符用于动态分配的对象时。
3. 对象作为函数返回值时,其中临时对象会在表达式结束后被销毁。
4. 程序结束时,全局对象会被自动销毁。
2.2.2 析构函数的注意事项
析构函数的设计需要非常谨慎,以确保对象被正确销毁,释放所有资源。以下是几个设计析构函数时需要注意的事项:
- 析构函数不应该抛出异常。
- 析构函数应当提供对资源释放的访问权限,而不能将其设置为private。
- 析构函数一般应为虚函数,尤其是当类设计为基类时,以确保派生类的正确析构。
通过本节的介绍,我们了解到构造函数和析构函数是类设计中不可或缺的元素,它们在对象的创建和销毁过程中扮演着关键角色。合理使用构造函数和析构函数可以避免资源泄露,保证程序的健壮性和可维护性。在下一章中,我们将探讨C++中对数据访问的控制,进一步深入理解类的封装特性。
3. 访问控制修饰符(public、private、protected)
3.1 三者的基本概念和区别
3.1.1 public成员的作用域
在面向对象编程中,类的成员可以被声明为 public 、 private 或 protected 。 public 成员定义了一个接口,它允许类的用户直接访问该成员。在类的外部, public 成员函数和变量可以被任何代码访问。
class Example {
public:
int publicVar; // 公开变量
void publicMethod() {
// 公开方法
}
};
int main() {
Example obj;
obj.publicVar = 10; // 直接访问公开变量
obj.publicMethod(); // 直接调用公开方法
return 0;
}
在上面的示例中, publicVar 变量和 publicMethod 函数被声明为 public ,这意味着它们可以直接被类的外部代码访问。
3.1.2 private成员的作用域
private 成员只对类的内部代码可见,它们不能直接被类的实例(对象)或者派生类访问。私有成员的使用为类的状态和行为提供了一种封装形式。
class Example {
private:
int privateVar; // 私有变量
public:
void setPrivateVar(int value) {
privateVar = value; // 通过公共接口设置私有变量
}
};
int main() {
Example obj;
// obj.privateVar = 10; // 错误:不能直接访问私有变量
obj.setPrivateVar(10); // 正确:通过公共方法设置私有变量
return 0;
}
在 main 函数中尝试直接访问 privateVar 是不允许的,我们必须使用类提供的 setPrivateVar 方法来访问它。
3.1.3 protected成员的作用域
protected 成员既不像 public 成员那样完全公开,也不像 private 成员那样完全隐藏。 protected 成员在类的内部和派生类中是可见的,但在类的外部则不可见。
class Base {
protected:
int protectedVar; // 受保护变量
public:
void setProtectedVar(int value) {
protectedVar = value;
}
};
class Derived : public Base {
public:
void accessProtectedVar() {
setProtectedVar(20); // 通过基类的公共方法访问受保护变量
}
};
int main() {
Derived obj;
// obj.protectedVar = 20; // 错误:不能直接访问受保护变量
obj.accessProtectedVar(); // 正确:通过派生类访问受保护变量
return 0;
}
在这个例子中, protectedVar 是 Base 类的受保护成员,它不能被 main 函数中的 Derived 类对象直接访问,但可以通过 Derived 类的公共方法间接访问。
3.2 访问控制修饰符在继承中的应用
3.2.1 继承时的权限变化规则
在C++中,派生类继承基类时,基类的成员访问权限会受到一些规则的影响。具体规则如下:
public继承:基类中的public成员在派生类中仍然是public,protected成员仍然是protected,private成员在派生类中不可访问。protected继承:基类中的public和protected成员在派生类中都变成了protected。private继承:基类中的public和protected成员在派生类中都变成了private。
class Base {
public:
int publicVar; // 公有变量
protected:
int protectedVar; // 受保护变量
private:
int privateVar; // 私有变量
};
class DerivedPublic : public Base {
public:
void accessMembers() {
publicVar = 1; // 可以访问
protectedVar = 2; // 可以访问
// privateVar = 3; // 错误:不可访问
}
};
class DerivedProtected : protected Base {
public:
void accessMembers() {
publicVar = 1; // 可以访问
protectedVar = 2; // 可以访问
// privateVar = 3; // 错误:不可访问
}
};
class DerivedPrivate : private Base {
public:
void accessMembers() {
publicVar = 1; // 可以访问
protectedVar = 2; // 可以访问
// privateVar = 3; // 错误:不可访问
}
};
3.2.2 访问控制与多态的关系
多态是面向对象程序设计的一个核心概念,它允许派生类重写基类中的 virtual 成员函数,并通过基类指针或引用来调用派生类的函数实现。这种多态行为依赖于合理的访问控制,通常通过虚函数来实现。
class Base {
public:
virtual void show() {
std::cout << "Base class show function" << std::endl;
}
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived class show function" << std::endl;
}
};
int main() {
Base* bptr;
Base b;
Derived d;
bptr = &b;
bptr->show(); // 输出: Base class show function
bptr = &d;
bptr->show(); // 输出: Derived class show function,展示了多态
return 0;
}
在这个例子中, Base 类有一个虚函数 show() , Derived 类重写了这个函数。通过基类指针 bptr 指向不同的对象,我们可以调用 show() 方法,并得到多态行为。这说明,访问控制修饰符与多态的实现是紧密相关的。
通过本章节的介绍,读者应该能够理解访问控制修饰符在C++面向对象编程中的重要性,以及如何在继承和多态中有效地使用这些修饰符。接下来的章节将继续深入探讨继承和多态等更高级的概念。
4. 继承的概念和用法
继承是面向对象编程中的一个基本概念,它允许我们创建类的层级结构,通过继承,我们可以将一些通用的功能放在基类中,而派生类则可以继承这些功能,并添加或覆盖特定的方法以满足自己的需求。继承机制在C++中非常强大,但也需要谨慎使用,以避免出现不必要的问题,如资源泄漏和意外的对象行为。
4.1 继承的基本概念和好处
继承的概念来自于现实世界的类比。比如,汽车继承自交通工具,汽车类会有交通工具的所有属性和行为,同时还会添加自己特有的属性和行为。
4.1.1 继承的语法结构
在C++中,继承是通过使用冒号 : 来表示的。派生类会声明它要继承自哪个基类。例如:
class Vehicle {
public:
void startEngine() { /* ... */ }
// ... 其他成员 ...
};
class Car : public Vehicle {
public:
void startEngine() override { /* ... */ }
// ... 其他成员 ...
};
在上面的代码中, Car 类继承自 Vehicle 类,它使用 public 继承方式,这意味着 Car 对象将能够使用 Vehicle 的所有公有成员。继承可以是 public 、 protected 或 private ,每种方式有不同的访问权限。
4.1.2 继承的类型与区别
C++支持多种继承类型,这些继承类型定义了派生类和基类之间的访问权限。
public继承:基类的公有成员和保护成员保持原有的状态,公有成员在派生类中依然是公有的,保护成员依然是保护的。protected继承:基类的公有成员和保护成员在派生类中都会变成保护的。private继承:基类的公有成员和保护成员在派生类中都会变成私有的。
继承类型的选择取决于派生类对基类成员的访问需求。
4.2 继承中的构造与析构顺序
继承的另一个关键方面是构造函数和析构函数的调用顺序。理解这一点对于管理对象生命周期,特别是资源分配和释放非常重要。
4.2.1 基类与派生类的构造顺序
构造函数的调用顺序总是先基类后派生类。这是因为派生类对象在构造时需要先构造它的基类部分。
class Base {
public:
Base() { std::cout << "Base constructor\n"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived constructor\n"; }
};
// 执行
Derived d;
输出将会是:
Base constructor
Derived constructor
4.2.2 基类与派生类的析构顺序
与构造顺序相反,析构函数的调用顺序是先派生类后基类。这是为了确保派生类在其基类部分被销毁之前完全释放它自己的资源。
// 继续上面的代码
~Derived() { std::cout << "Derived destructor\n"; }
~Base() { std::cout << "Base destructor\n"; }
// 执行
d.~Derived();
输出将会是:
Derived destructor
Base destructor
正确的构造和析构顺序对于防止资源泄露至关重要,特别是当派生类中分配了额外资源时。理解这些顺序能够帮助我们编写出更健壮的代码。
5. 多态的实现方法和虚函数
5.1 多态的基本原理与实现
5.1.1 多态的定义和意义
多态(Polymorphism)是面向对象编程中的核心概念之一,它允许同一操作作用于不同的对象,可以产生不同的效果。在C++中,多态分为编译时多态和运行时多态。编译时多态主要通过函数重载和运算符重载来实现,而运行时多态则是通过虚函数(virtual functions)来实现的。运行时多态提供了接口的统一特性,使得通过父类指针或引用调用同一个方法时,根据对象的实际类型调用相应的函数版本,这种特性极大地提高了代码的可扩展性和可维护性。
5.1.2 实现多态的条件
为了实现运行时多态,我们需要满足以下几个条件:
- 必须有继承关系。即存在一个基类和至少一个派生类。
- 基类中必须至少有一个虚函数(virtual function)。该虚函数可以是成员函数或纯虚函数(pure virtual function)。
- 通过基类指针或引用调用虚函数时,会根据对象的实际类型来决定调用哪个函数版本。
5.2 虚函数和纯虚函数的使用
5.2.1 虚函数的声明与重写
在C++中,如果基类中的成员函数被声明为 virtual ,那么其派生类中的同名函数就会自动成为该虚函数的重写版本。当通过基类的指针或引用调用虚函数时,实际调用的是对象的实际类型所对应的函数版本。这为派生类提供了机会去实现或修改基类的行为。
#include <iostream>
class Base {
public:
virtual void display() const {
std::cout << "Base class display." << std::endl;
}
};
class Derived : public Base {
public:
void display() const override {
std::cout << "Derived class display." << std::endl;
}
};
int main() {
Base* b = new Base();
Base* d = new Derived();
b->display(); // 输出: Base class display.
d->display(); // 输出: Derived class display.
delete b;
delete d;
return 0;
}
5.2.2 纯虚函数与抽象类的概念
纯虚函数是一种特殊的虚函数,其声明中包含 = 0 ,表明该函数没有具体的实现。含有纯虚函数的类被称为抽象类,抽象类不能直接实例化对象,但可以被派生类继承并实现其纯虚函数。
class AbstractBase {
public:
virtual void pureVirtualFunction() const = 0; // 纯虚函数声明
};
class ConcreteDerived : public AbstractBase {
public:
void pureVirtualFunction() const override {
std::cout << "ConcreteDerived implements pureVirtualFunction." << std::endl;
}
};
int main() {
AbstractBase* base = new ConcreteDerived();
base->pureVirtualFunction(); // 输出: ConcreteDerived implements pureVirtualFunction.
delete base;
return 0;
}
在上面的代码中, AbstractBase 类由于包含纯虚函数而成为了抽象类,无法创建实例,但是 ConcreteDerived 类继承了 AbstractBase 并实现了其纯虚函数,从而可以创建 ConcreteDerived 类型的实例。
多态的实际应用和性能影响
多态是C++中实现组件化编程、插件系统以及设计模式(如策略模式、状态模式等)的关键技术。通过虚函数和继承,可以创建一组行为相似但具体实现不同的对象,从而使得程序能够适应更广泛的变化需求。
性能影响
多态的实现需要在运行时进行函数跳转,这通常意味着性能开销。特别是当使用虚函数时,编译器可能需要维护一个虚函数表(vtable),对象内部需要保存指向vtable的指针,这会增加内存使用。因此,在性能敏感的应用中,过度使用虚函数可能导致性能问题。合理地使用多态、避免不必要的虚函数调用,可以减少性能损耗。
6. C++类与对象进阶应用
在C++中,类和对象的进阶应用为我们提供了一种强大且灵活的方式来设计和实现复杂的系统。本章我们将深入探讨类模板的应用与实现、类的友元函数和友元类的概念,以及操作符重载的技巧和方法。
6.1 类模板的应用与实现
6.1.1 类模板的概念与定义
类模板是C++中用于定义可重用类结构的泛型编程工具。它允许我们创建一个通用的类设计,该设计可以在创建对象时由编译器实例化为具有特定数据类型的特定版本。类模板通常用于创建通用的容器类,如链表、队列、栈等。
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& element);
void pop();
T top() const;
bool isEmpty() const;
};
6.1.2 类模板的实例化与使用
类模板需要在使用之前进行实例化,以便为特定的数据类型创建一个具体的类。实例化类模板时,我们只需提供所需的数据类型作为模板参数。
Stack<int> intStack; // 实例化一个int类型的栈
intStack.push(10);
Stack<std::string> stringStack; // 实例化一个std::string类型的栈
stringStack.push("Hello");
类模板不仅可以实例化为基本数据类型,还可以实例化为用户定义的类型,甚至是其他模板类型。
6.2 类的友元函数和友元类
6.2.1 友元函数的作用与限制
友元函数是类允许外部函数访问其私有(private)和保护(protected)成员的机制。它们被定义为友元,意味着它们不是类的成员函数,但它们有权访问类的非公有成员。友元函数可以是一个普通函数或另一个类的成员函数。
class Rectangle {
private:
int width, height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
// 声明一个友元函数
friend int area(const Rectangle&);
};
// 定义友元函数
int area(const Rectangle& rect) {
return rect.width * rect.height;
}
友元函数虽然提供了灵活性,但它们可能会破坏封装性,因此应谨慎使用。
6.2.2 友元类的概念及实现
友元类是拥有访问另一个类的所有成员权限的类。友元关系不是对称的,即如果类A是类B的友元类,并不意味着类B也是类A的友元类。
class Point {
private:
int x, y;
friend class Rectangle; // 声明Rectangle为友元类
public:
Point(int x, int y) : x(x), y(y) {}
};
class Rectangle {
private:
Point topLeft, bottomRight;
public:
Rectangle(Point t, Point b) : topLeft(t), bottomRight(b) {}
int area() {
int width = bottomRight.x - topLeft.x;
int height = bottomRight.y - topLeft.y;
return width * height;
}
};
友元类允许两个类共享彼此的私有和保护成员,但过度使用会使得类之间的耦合度增加。
6.3 操作符重载的技巧和方法
6.3.1 操作符重载的基本原则
操作符重载是将C++语言中的运算符应用于用户定义类型的对象的过程。操作符重载为这些对象提供了一种自然的语法,使得与内置类型的交互更加直观。
当重载操作符时,需要遵循以下基本原则:
- 不能创建新的操作符,只能重载现有的操作符。
- 除了少数几个操作符外,大多数操作符都可以被重载。
- 不能改变操作符的优先级。
- 不能改变操作数的个数(如一元或二元操作符)。
6.3.2 常见操作符的重载实例
下面是一个重载加法操作符的实例,它允许两个类对象相加。
class Complex {
private:
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
// 重载加法操作符
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
};
Complex c1(5.0, 3.0);
Complex c2(2.0, 1.0);
Complex c3 = c1 + c2; // c3现在是7.0 + 4.0i
操作符重载提供了强大而直观的方法来扩展C++库,使得用户自定义类型的操作更加自然。然而,应该避免不直观的或误导性的重载,以免代码难以理解。
简介:C++编程语言以类和对象为核心,支持面向对象编程。本文深入介绍类的定义、对象的创建、构造函数和析构函数的作用,以及访问控制修饰符、继承和多态等面向对象编程的重要概念。通过具体示例,本文将帮助读者理解如何在C++中通过类和对象实现封装、继承和多态,从而编写出结构化、模块化的代码。
更多推荐


所有评论(0)