- 面向对象程序的主要特点;抽象,封装,继承,和多态
抽象
- 指对具体问题(对象)进行概括,抽出一类对象的公共性质并加以描述的过程
- 对一个问题的抽象包括:数据抽象和行为抽象(或称为功能抽象,代码抽象),前者描述某类对象的属性或状态(此类对象与其他类对象区别的特征),后者描述某类对象的共同行为和功能特征
- 例如,实现一个时钟程序:int hour,int minute,int second;//数据抽象 showtime(),settime();//功能抽象
封装
- 将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体(将数据与操作数据的函数代码进行有机的结合)形成“类”,其中的数据和函数都是类的成员
- 例如时钟类
-
class Clock{ private: int hour,int minute,int second; public: void showtime(); void settime(); };
多态
- 指一段程序能够处理多种类型对象的能力
- 在c++中多态通过强制多态(),重载多态,参数多态,包含多态来实现
- 强制多态(Coercion Polymorphism):
- 强制多态是通过语义操作,将操作对象的类型强行加以变换,以符合函数或操作符的要求。
- 这不是任意两个类型之间都可以进行的,通常需要在不同类型之间执行特定的转换操作。
- 强制多态的原则是将值集较小的类型(占用存储空间较小)转换为值集较大的类型(占用存储空间较大),以避免值的损伤。
class Base { public: operator int() const { // 假设某种转换逻辑 return 42; // 返回一个整数值作为示例 } }; int main() { Base b; int i = b; // 隐式调用Base的转换运算符,实现了强制多态的效果 return 0; }
- 重载多态(Overloading Polymorphism):
- 重载多态是指允许存在多个同名函数,但它们的参数列表(参数个数或类型)不同。
- 编译器根据调用时提供的参数类型和数量来判断应该调用哪个函数。
- 这种多态性在静态类型语言如C++、Java和C#中非常常见。
- 主要体现函数重载和运算符重载
-
void print(int x) { std::cout << "Printing an int: " << x << std::endl; } void print(double x) { std::cout << "Printing a double: " << x << std::endl; } int main() { print(10); // 调用print(int) print(3.14); // 调用print(double) return 0; }
- 参数多态(Parametric Polymorphism),也称作泛型多态:
- 参数多态涉及函数模板和类模板的使用。
- 它允许我们编写与类型无关的代码,从而增加代码的复用性。
- 在实例化模板时,编译器会为特定的类型生成相应的代码。
- 在C++中,参数多态(Parametric Polymorphism)或泛型多态主要通过模板(templates)来实现,这包括函数模板和类模板。以下是使用类模板来实现参数多态的一个例子
-
// 定义一个类模板 template <typename T> class MyContainer { private: T* data; size_t size; public: // 构造函数 MyContainer(size_t size) : size(size), data(new T[size]) {} // 析构函数 ~MyContainer() { delete[] data; } // 添加元素到容器 void add(T value) { // 假设这里只是简单地添加到数组的末尾(实际中可能需要考虑边界) data[size - 1] = value; } // 获取容器中的元素 T get(size_t index) { // 假设index是有效的 return data[index]; } // ... 其他成员函数 ... }; // 使用类模板的示例 int main() { // 创建一个存储int的MyContainer对象 MyContainer<int> intContainer(5); for (size_t i = 0; i < intContainer.size; ++i) { intContainer.add(i); } std::cout << "Element at index 2: " << intContainer.get(2) << std::endl; // 创建一个存储double的MyContainer对象 MyContainer<double> doubleContainer(3); doubleContainer.add(1.1); doubleContainer.add(2.2); doubleContainer.add(3.3); std::cout << "Element at index 1: " << doubleContainer.get(1) << std::endl; return 0; }
- 包含多态(Inclusion Polymorphism):
- 包含多态体现在一个类包含另一个类的对象,或者一个类型及其子类型可以共享相同的操作。
- 这种多态性通常需要在运行时进行类型检查。
- 它展示了面向对象编程中“一个接口,多种实现”的思想,是面向对象编程的灵魂之一。
- 包含多态通常通过继承和虚函数实现。
-
class Animal { public: virtual void makeSound() const { std::cout << "The animal makes a sound" << std::endl; } }; class Dog : public Animal { public: void makeSound() const override { std::cout << "The dog barks" << std::endl; } }; class Cat : public Animal { public: void makeSound() const override { std::cout << "The cat meows" << std::endl; } }; void makeAnimalSound(Animal& animal) { animal.makeSound(); // 运行时多态:调用正确的makeSound版本 } int main() { Dog dog; Cat cat; Animal& animalRef1 = dog; Animal& animalRef2 = cat; makeAnimalSound(animalRef1); // 输出"The dog barks" makeAnimalSound(animalRef2); // 输出"The cat meows" return 0; }
继承:
- 在C++中,继承(Inheritance)是一种面向对象编程的重要特性,它允许一个类(派生类/子类)继承另一个类(基类/父类)的属性和方法。继承的类可以获取基类的所有公共(public)和保护(protected)成员,并且可以添加或覆盖自己的成员。
-
#include <iostream> #include <string> // 基类 Animal class Animal { public: Animal(const std::string &name) : name(name) {} virtual ~Animal() {} // 虚析构函数,用于确保派生类的正确析构 // 公共方法 void eat() { std::cout << name << " eats." << std::endl; } // 公共属性(通常不推荐直接暴露属性,但为了示例简洁性) std::string name; // 纯虚函数(可选),如果基类包含纯虚函数,则基类成为抽象类,不能直接实例化 virtual void sound() const = 0; // 纯虚函数 }; // 派生类 Dog,继承自 Animal class Dog : public Animal { public: Dog(const std::string &name) : Animal(name) {} // 覆盖基类的 sound 方法 void sound() const override { std::cout << name << " barks." << std::endl; } // Dog 特有的方法 void playFetch() { std::cout << name << " plays fetch." << std::endl; } }; // 派生类 Cat,继承自 Animal class Cat : public Animal { public: Cat(const std::string &name) : Animal(name) {} // 覆盖基类的 sound 方法 void sound() const override { std::cout << name << " meows." << std::endl; } // Cat 特有的方法 void playWithYarn() { std::cout << name << " plays with yarn." << std::endl; } }; int main() { Dog myDog("Buddy"); myDog.eat(); // 调用基类的 eat 方法 myDog.sound(); // 调用派生类的 sound 方法 myDog.playFetch(); // 调用 Dog 特有的 playFetch 方法 Cat myCat("Whiskers"); myCat.eat(); // 调用基类的 eat 方法 myCat.sound(); // 调用派生类的 sound 方法 myCat.playWithYarn(); // 调用 Cat 特有的 playWithYarn 方法 return 0; }
-
类的定义
语法形式为:
class{
public:
外部接口
protected:
保护型成员
private;
私有成员
}
其中
public(公有):
- 公有成员可以在类的外部被访问。
- 公有成员函数通常用于实现类的外部接口。
- 公有数据成员通常是不推荐的,因为它们破坏了封装性(封装是面向对象编程的四大基本特性之一)。
protected(保护):
- 保护成员可以在类的内部(包括类的成员函数、友元函数和派生类)被访问,但不能在类的外部被直接访问。
- 保护成员通常用于实现继承层次结构中的共享功能,这些功能对基类用户是隐藏的,但对派生类是可见的。
private(私有):
- 私有成员只能在类的内部(即类的成员函数和友元函数)被访问。
- 私有成员是类的内部实现细节,对类的用户是隐藏的。
- 私有成员是封装性的关键部分,它确保类的用户只能通过类提供的公有接口来与类进行交互。
我们可以在类中对数据成员进行初始化:
#include<iostream>
using namespace std;
class Clock {
public:
int hour = 0, minute = 0, second = 0;
void printTime() {
cout << hour << ":" << minute << ":" << second << endl;
}
};
int main() {
// 创建一个 Clock 类的对象
Clock myClock;
// 访问 myClock 对象的 hour 成员并打印它 (hour为公有成员)
cout << myClock.hour << endl;
// 使用我们添加的 printTime 函数来打印完整的时间
myClock.printTime();
return 0;
}
类内初始化必须以等号或者花括号表示hour{0} hour=0;
没有初始值的成员会被默认初始化
对象
- 语法形式
类名 对象名; Clock myClock;
- 对象所占据内存仅用于存放数据成员,函数成员不在每个对象中存储副本,每个函数代码在内存中只占一小部分
- 对象所占据内存仅用于存放数据成员:
- 在面向对象编程中,对象是基于类创建的实例。类定义了对象的结构,包括数据成员(也称为属性或字段)和函数成员(也称为方法或函数)。
- 当我们创建一个对象时,这个对象在内存中会有一块空间来存储其数据成员的值。这些数据成员的值是唯一的,每个对象都有自己的数据成员副本。
- 例如,如果我们有一个
Person
类,它有两个数据成员:name
和age
。当我们创建两个Person
对象时,这两个对象将分别有自己的name
和age
值的存储空间。 - 函数成员不在每个对象中存储副本:
- 与数据成员不同,函数成员(即方法)不是存储在对象本身的内存中的。相反,函数成员(或更准确地说是函数成员的代码)在程序的内存中是共享的,不随对象的创建而复制。
- 当我们调用一个对象的函数成员时,我们实际上是在调用一个与该类相关联的函数,这个函数是共享的,而不是每个对象都有一个该函数的副本。
- 函数成员知道它们是被哪个对象调用的,因为当函数被调用时,会传递一个指向调用对象的隐式指针(在C++中是
this
指针)。这使得函数成员能够访问和修改该对象的数据成员。 - 每个函数代码在内存中只占一小部分:
- 函数代码(无论是类的成员函数还是全局函数)在内存中通常都只占一小部分空间。这是因为函数代码是编译后的机器指令,这些指令通常比数据(特别是复杂的数据结构或大量数据)要小得多。
- 当多个对象调用同一个函数时,它们实际上都在执行同一段代码,只是传递了不同的参数和隐式地使用了不同的
this
指针(在C++中)。 - 对象在内存中主要存储其数据成员的值,而函数成员的代码是共享的,不随对象的创建而复制。这种设计提高了内存使用效率,并允许对象之间共享函数实现。
定义完类和对象后便可以访问对象成员:
对象名.数据成员名 myClock.hour 对象名.函数成员名(参数表)myClock.printTime()
注意:在类外部只能访问public
类的成员函数中可以访问所以成员
类的成员函数:
在类的成员函数中,既可以访问目的对象的私有成员也可以访问当前类的其他对象的私有成员
类的初始化和清理工作主要由两个特殊的成员函数完成(构造和析构函数)
构造函数(Constructor)
构造函数是一个特殊的成员函数,当创建类的实例时自动被调用。它没有返回类型(也不是void
),与类名相同。
当自己完成构造函数后系统便不会在生成构造函数
若在A类中定义了带参数构造函数
如果写A a;语法错误错误
class MyClass {
public:
MyClass(int value) {
// 构造函数体
// 使用传入的value来初始化成员变量等
}
// ... 其他成员函数 ...
};
// 创建对象时自动调用构造函数
MyClass obj(42);
委托构造函数
class MyClass {
public:
MyClass(int value) {
// 构造函数体
// 使用传入的value来初始化成员变量等
}
MyClass() :MyClass(0){
}
// ... 其他成员函数 ...
};
// 创建对象时自动调用构造函数
MyClass obj(42);
析构函数(Destructor)
析构函数是一个特殊的成员函数,当对象离开其作用域或被显式删除时自动被调用。它没有参数,没有返回类型,与类名相同,但前面有一个波浪号(~
)。
class MyClass {
public:
~MyClass() {
// 析构函数体
// 释放资源,如动态分配的内存等
}
// ... 其他成员函数 ...
};
// 当obj离开作用域时,析构函数自动被调用
{
MyClass obj;
} // 在这里,obj的析构函数被调用
拷贝构造函数(Copy Constructor)
拷贝构造函数用于从一个已存在的对象创建一个新的对象。它的参数是一个对同类型对象的引用(通常是常量引用,以防止意外修改原对象)。
class MyClass {
public:
MyClass(const MyClass& other) {
// 拷贝构造函数体
// 使用other的成员来初始化新对象的成员
}
// ... 其他成员函数 ...
};
MyClass obj1(42);
MyClass obj3 = obj1;
MyClass obj2(obj1); // 调用拷贝构造函数
成员函数的特点
定义在类内部:成员函数是类定义的一部分,它们与类的数据成员(字段或属性)一起定义了类的接口和行为。
函数原型写在类中,可以在类外实现函数
语法结构:
class A{
private:
int x;
public:
void setA(int n)
{
x = n;
}
};
class A{
private:
int x;
public:
void setA(int n);
};
void A::setA(int n)
{
x = n;
}
通过对象调用:要调用成员函数,你需要先创建一个类的实例(对象)。然后,你可以使用点操作符(.
)或箭头操作符(->
,在指针的情况下)来调用该对象的成员函数。
#include <iostream>
class MyClass {
private:
int myPrivateVar;
public:
// 构造函数
MyClass(int value) : myPrivateVar(value) {}
// 成员函数(或方法)
// 获取私有成员的值
int getValue() const {
return myPrivateVar;
}
// 设置私有成员的值
void setValue(int value) {
myPrivateVar = value;
}
// 输出私有成员的值
void printValue() const {
std::cout << "The value is: " << myPrivateVar << std::endl;
}
};
int main() {
// 创建MyClass的实例(对象)
MyClass myObject(42);
// 通过对象调用成员函数
std::cout << "Initial value: ";
myObject.printValue(); // 输出私有成员的值
// 修改私有成员的值
myObject.setValue(100);
// 再次通过对象调用成员函数以确认值已更改
std::cout << "Updated value: ";
myObject.printValue(); // 输出更新后的私有成员的值
return 0;
}
注意,当我们通过对象调用成员函数时,我们使用的是点操作符(.
)来访问成员函数。这是因为在C++中,点操作符用于表示对象的成员访问。对于指针类型的对象,我们会使用箭头操作符(->
)来访问其成员函数。访问类的成员:成员函数可以访问类的私有、保护或公有成员。这允许成员函数在内部操作类的状态,同时保持封装性。
实现特定功能:成员函数通常实现与类相关的特定功能。例如,一个Person
类可能有一个setName
成员函数来设置人的名字,和一个getName
成员函数来获取人的名字。
可以有参数和返回值:成员函数可以像常规函数一样接受参数并返回值。这些参数和返回值可以是任何类型,包括类类型本身。
可以是静态的:静态成员函数属于类本身,而不是类的实例。它们可以通过类名直接调用,并且不能访问类的非静态成员(除非它们是mutable
的)。
静态成员函数属于类本身,而不是类的某个实例(对象)。这意味着它们不需要通过类的实例来调用,而可以直接通过类名来调用。静态成员函数只能访问类的静态成员(包括静态数据成员和其他静态成员函数),而不能访问类的非静态成员。
#include <iostream>
class MyClass {
private:
static int staticVar; // 静态数据成员
public:
MyClass(int value) {
// 这里不能直接访问或修改静态数据成员,因为它属于类本身
// 但可以在构造函数中调用静态成员函数来初始化它
setStaticVar(value);
}
// 静态成员函数
static void setStaticVar(int value) {
staticVar = value; // 可以访问和修改静态数据成员
}
static int getStaticVar() {
return staticVar; // 返回静态数据成员的值
}
// 非静态成员函数
void printValue() const {
// 非静态成员函数不能直接访问静态数据成员
// 但可以通过类名和作用域解析运算符来访问它
std::cout << "Static variable value: " << MyClass::getStaticVar() << std::endl;
}
};
// 静态数据成员必须在类外部定义
int MyClass::staticVar = 0;
int main() {
MyClass myObject(42); // 创建对象,构造函数调用静态成员函数来设置静态变量
// 通过类名调用静态成员函数
std::cout << "Static variable value: " << MyClass::getStaticVar() << std::endl;
// 也可以通过对象调用静态成员函数(但通常不推荐这样做,因为不够清晰)
std::cout << "Static variable value (via object): " << myObject.getStaticVar() << std::endl;
// 调用非静态成员函数来打印静态变量的值
myObject.printValue();
return 0;
}
可以是虚函数:虚函数允许在派生类中对基类函数进行重写(覆盖)。这是实现多态性的关键,使得我们可以使用基类指针或引用来调用派生类的函数。
可以是常量成员函数:常量成员函数(在函数声明后加上const
关键字)不能修改类的任何成员(除非它们是mutable
的)。这有助于确保对象的常量性不被破坏。
常量成员函数意味着该函数不会修改调用它的对象的任何成员变量(除非这些成员变量被声明为mutable
)。常量成员函数在声明时需要在函数声明的末尾添加const
关键字。
常量成员函数主要用于那些不需要修改对象状态的成员函数,比如获取对象状态的函数。常量成员函数可以在常量对象上被调用,因为它们保证不会修改对象。
#include <iostream>
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
// 常量成员函数,仅读取成员变量
int getValue() const {
return value;
}
// 非常量成员函数,可以修改成员变量
void setValue(int v) {
value = v;
}
// 另一个常量成员函数,打印成员变量的值
void printValue() const {
std::cout << "Value: " << value << std::endl;
}
};
int main() {
const MyClass constObj(42); // 创建一个常量对象
// 可以调用常量成员函数
std::cout << "Constant object value: ";
constObj.printValue(); // 输出:Value: 42
// 不能调用非常量成员函数,因为这会尝试修改常量对象
// constObj.setValue(100); // 编译错误
MyClass mutableObj(10); // 创建一个非常量对象
// 可以调用常量成员函数和非常量成员函数
std::cout << "Mutable object value: ";
mutableObj.printValue(); // 输出:Value: 10
mutableObj.setValue(100);
std::cout << "Mutable object value after set: ";
mutableObj.printValue(); // 输出:Value: 100
return 0;
}
可以是内联成员函数:内联成员函数(在函数声明前加上inline
关键字)通常会在编译时内联展开,以减少函数调用的开销。但请注意,内联只是一种建议,编译器可以选择忽略它。
可以是友元函数:友元函数是一种特殊的非成员函数,它可以访问类的私有和保护成员。但请注意,过度使用友元函数可能会破坏封装性。