1. 封装
在C++中,封装是一种将数据和操作数据的函数封装在一起的机制。它可以将数据隐藏起来,只提供公共接口给外部使用,从而保证数据的安全性和一致性。
封装的目的是将数据和相关操作封装在一起形成一个类,对外部使用者隐藏内部的实现细节。这样做的好处是可以隐藏数据的具体实现,提高代码的安全性和可维护性。
在C++中,封装主要通过类和访问控制来实现。类将数据成员和成员函数封装在一起,通过访问控制符(public、private、protected)来控制成员的访问权限。数据成员通常被声明为私有(private),只能通过公有(public)的成员函数来访问、修改数据。
封装的优点包括:
-
隐藏实现细节:封装使得实现细节对外部使用者不可见,只提供了一个抽象的接口。这样可以避免外部使用者了解或依赖于具体的实现细节,提高代码的安全性和可维护性。
-
简化接口:通过封装,可以将一组相关的数据和操作组织在一起,形成一个类。外部使用者只需要使用类提供的公共接口,而不需要了解内部的实现细节,从而简化了接口的使用。
-
提高代码复用性:类可以被多次实例化,每个实例都具有相同的数据结构和操作方法。这样可以提高代码的复用性,避免了重复编写相同功能的代码。
-
隔离变化:封装可以将类的实现细节与外部使用者分离开来,使得变化的影响范围最小。当类的实现发生变化时,只需要修改类的内部,而不需要修改外部使用者的代码。
总之,封装是C++中一种重要的特性,可以提高代码的安全性、可维护性和复用性。通过合理的封装,可以隐藏实现细节,简化接口,并隔离变化,使得代码更加健壮和可扩展。
1> 访问控制符,封装中的护城河
C++访问控制符用于控制类的成员(包括变量和函数)的访问权限,包括三种控制符:
-
public: 公有控制符,表示成员可以在类的内部和外部进行访问。任何地方都可以访问公有成员。
-
private: 私有控制符,表示成员只能在类的内部进行访问,外部无法直接访问私有成员。可以通过类的公有成员函数来间接访问私有成员。
-
protected: 保护控制符,类似于私有控制符,表示成员只能在类的内部进行访问,外部无法直接访问保护成员。不同的是,派生类可以直接访问基类的保护成员。
通过访问控制符,可以实现类的封装性和数据隐藏。公有成员提供给外部访问,私有成员只能在类内部使用,保护成员提供给派生类使用。这样可以防止外部直接修改类的私有成员,从而提高类的安全性和数据的完整性。
C++类中的默认控制符是private,与之区分的是C中的struct,它的默认控制符是public。
2> friend 封装中的双刃剑
在C++中,友元(friend)是一种特殊的函数或类,它可以访问另一个类中的私有成员。友元函数或类可以在被访问的类中声明为友元,从而被授权访问私有成员。
友元函数在类声明中使用关键字"friend"声明,如下所示:
class MyClass {
public:
MyClass(int num) : number(num) {}
friend void friendFunction(MyClass& obj);
private:
int number;
};
void friendFunction(MyClass& obj) {
// 可以访问MyClass中的私有成员
std::cout << "Private member of MyClass accessed: " << obj.number << std::endl;
}
在这个例子中,friendFunction函数被声明为MyClass的友元函数,因此它可以直接访问MyClass中的私有成员number。
友元类的声明方式类似于友元函数,只是将类名作为关键字"friend"后的参数,如下所示:
class MyClass {
public:
MyClass(int num) : number(num) {}
friend class FriendClass;
private:
int number;
};
class FriendClass {
public:
void accessPrivateMember(MyClass& obj) {
// 可以访问MyClass中的私有成员
std::cout << "Private member of MyClass accessed: " << obj.number << std::endl;
}
};
在这个例子中,FriendClass被声明为MyClass的友元类,因此它可以直接访问MyClass中的私有成员number。
需要注意的是,友元函数或类的声明并不属于类的成员函数或成员类。因此,在使用友元函数或类时,应在类的声明中将其声明为友元,而在类的实现中定义和实现它们。
3> 重载 封装中的套路
C++中的重载(Overload)是指在同一个作用域内,可以定义多个同名函数,但其参数列表必须不同。重载的函数可以根据不同的参数类型和个数进行调用,使得程序具有更灵活的使用性。
C++中函数的重载可以通过以下几种方式实现:
1. 参数个数不同:函数名相同,但参数个数不同,可以实现重载。例如:
void foo(int x);
void foo(int x, int y);
2. 参数类型不同:函数名相同,但参数类型不同,可以实现重载。例如:
void foo(int x);
void foo(double x);
3. 参数顺序不同:函数名相同,但参数类型相同,但参数顺序不同,可以实现重载。例如:
void foo(int x, double y);
void foo(double y, int x);
4. 常量性不同:函数名相同,但参数类型相同,但其中一个函数是const成员函数,可以实现重载。例如:
class MyClass {
public:
void print() const;
void print();
};
需要注意的是,重载函数的返回类型并不参与函数重载的判断。
当调用一个重载函数时,编译器会根据函数调用时的实参类型和个数,选择最合适的重载函数进行调用。如果存在多个重载函数都能匹配调用参数,编译器会进行一系列的匹配规则判断,选择最佳匹配的重载函数。
重载函数的命名应该尽量清晰和准确,以避免产生歧义和误导。同时,在使用重载时,需要注意函数调用的实参类型和个数,确保调用的重载函数是符合预期的。
4> 静态成员
C++中的静态成员是类的成员,与类的实例对象无关,它被所有的类实例共享。而非静态成员是每个类实例独有的。
静态成员可以是静态变量或静态函数。
静态变量: 静态成员变量在类声明中以static关键字定义,也可以在类外部进行初始化。它在程序运行期间只存在一份内存空间,所有的类实例共享此变量。
例如:
class MyClass {
public:
static int myStaticVariable;
};
int MyClass::myStaticVariable = 10; // 类外部初始化静态变量
int main() {
cout << MyClass::myStaticVariable << endl; // 输出:10
MyClass::myStaticVariable = 20;
cout << MyClass::myStaticVariable << endl; // 输出:20
// 类的多个实例共享同一个静态变量
MyClass obj1;
MyClass obj2;
obj1.myStaticVariable = 30;
cout << obj2.myStaticVariable << endl; // 输出:30
return 0;
}
静态函数: 静态成员函数在类声明中以static关键字定义,它不依赖于类的实例对象而存在。它可以直接通过类名调用,无需创建类的实例对象。
例如:MyClass::myStaticFunction();
class MyClass {
public:
static void myStaticFunction() {
cout << "This is a static function." << endl;
}
};
int main() {
MyClass::myStaticFunction(); // 输出:This is a static function.
return 0;
}
5> 构造函数
C++构造函数是用于创建对象时自动调用的特殊成员函数。它的主要作用是初始化对象的数据成员。
构造函数的特点包括:
- 构造函数与类名相同,没有返回类型,可以有参数,也可以没有参数。
- 构造函数在对象创建时自动调用,用于初始化对象的数据成员。
- 如果没有定义构造函数,编译器会自动生成默认构造函数,用于创建对象时不进行初始化。
- 可以定义多个构造函数,称为构造函数的重载。根据传入的参数类型和数量的不同,编译器会根据具体的参数调用对应的构造函数。
构造函数的使用方法如下:
- 定义一个与类名相同的函数作为构造函数。
- 在该函数中初始化对象的数据成员。
- 创建对象时,可以直接调用构造函数进行初始化。
例如,假设有一个名为Person的类,包含姓名和年龄两个数据成员,可以定义一个构造函数来初始化这两个成员:
class Person {
private:
string name;
int age;
public:
Person(string n, int a) {
name = n;
age = a;
}
};
然后可以创建Person对象时直接调用构造函数进行初始化:
Person p("Alice", 25);
在这个例子中,构造函数带有两个参数,用于初始化name和age成员。创建对象时,可以通过传入相应的参数来初始化对象的数据成员。
在使用C++构造函数时,有一些注意事项需要注意:
-
构造函数没有返回类型:构造函数的名称与类名相同,但是没有返回类型,包括void。因此,在声明和定义构造函数时,不需要指定返回类型。
-
默认构造函数:如果没有定义任何构造函数,编译器会自动生成一个无参的默认构造函数。它会执行默认的对象初始化,即对类的数据成员进行默认初始化(如基本类型的数据成员被初始化为0,指针类型的数据成员被初始化为nullptr)。
-
重载构造函数:可以根据需要定义多个构造函数,它们具有不同的参数类型和数量。这样可以根据传入的参数不同,选择合适的构造函数进行对象的初始化。这种情况下,称为构造函数的重载。
-
初始化列表:在构造函数的函数体中,可以使用初始化列表来对数据成员进行初始化。初始化列表以冒号(:)开始,后面跟着用逗号分隔的成员初始化表达式。使用初始化列表可以更高效地初始化对象的数据成员。
-
拷贝构造函数:拷贝构造函数是一种特殊的构造函数,用于在创建对象时通过复制另一个对象的值来初始化新对象。如果没有显式定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数。
-
构造函数的重要性:构造函数在对象创建时自动调用,用于初始化对象的数据成员。它对于确保对象的正确初始化非常重要,可以避免在使用对象之前需要进行额外的初始化操作。
总之,构造函数是C++中用于创建对象时初始化数据成员的特殊函数。理解构造函数的重载、初始化列表和拷贝构造函数等概念,并注意构造函数的重要性,能够更好地使用C++的构造函数。
6> 构造函数重载
C++中的构造函数重载是指在同一个类中定义多个构造函数,每个构造函数具有不同的参数列表。通过构造函数重载,可以使得对象的创建更加灵活,可以根据不同的参数选择不同的构造函数来初始化对象的成员变量。
构造函数重载的实现方式如下:
-
构造函数的定义:在类的定义中声明多个构造函数,每个构造函数的参数列表不同。
-
构造函数的命名:构造函数的名称与类的名称相同。
-
参数列表的不同:构造函数的参数列表应该是唯一的,可以根据参数的类型、个数和顺序来区分。
构造函数重载示例代码如下:
#include <iostream>
class MyClass {
private:
int value;
public:
// 构造函数1,无参数
MyClass() : value(0) {
std::cout << "Constructor 1 called" << std::endl;
}
// 构造函数2,有一个整型参数
MyClass(int v) : value(v) {
std::cout << "Constructor 2 called" << std::endl;
}
// 构造函数3,有两个整型参数
MyClass(int v1, int v2) : value(v1 + v2) {
std::cout << "Constructor 3 called" << std::endl;
}
int getValue() {
return value;
}
};
int main() {
MyClass obj1; // 调用构造函数1
MyClass obj2(10); // 调用构造函数2
MyClass obj3(2, 3); // 调用构造函数3
std::cout << "obj1.value = " << obj1.getValue() << std::endl;
std::cout << "obj2.value = " << obj2.getValue() << std::endl;
std::cout << "obj3.value = " << obj3.getValue() << std::endl;
return 0;
}
输出结果为:
Constructor 1 called
Constructor 2 called
Constructor 3 called
obj1.value = 0
obj2.value = 10
obj3.value = 5
在这个示例中,我们定义了一个名为MyClass
的类,它包含了三个不同的构造函数。根据不同的参数列表,可以选择不同的构造函数来创建对象。在main
函数中,我们分别使用不同的方式来创建对象,并输出对象的值。
7> 析构函数
C++ 析构函数(Destructor)是一种特殊的成员函数,用于在对象的生命周期结束时进行清理工作,例如释放动态分配的内存、关闭文件等资源的释放。以下是关于C++析构函数的一些重要注意事项:
-
析构函数的命名规则:析构函数的名称与类名相同,但前面加上一个波浪线(~),表示它是一个析构函数。
-
没有参数和返回值:析构函数没有参数,也没有返回值,包括void。因此,声明和定义析构函数时,不需要指定参数类型和返回类型。
-
自动调用:析构函数在对象的生命周期结束时自动调用,通常是当对象超出范围(例如离开作用域)或者被显式删除时。
-
默认析构函数:如果没有显式定义析构函数,编译器会提供一个默认的析构函数,默认析构函数什么也不做。
-
手动定义析构函数:如果类中有动态分配的资源(例如使用new关键字分配的内存),则通常需要手动定义析构函数来释放这些资源,以防止内存泄漏。
-
拷贝构造函数和赋值运算符:如果类中包含指针成员变量,应该特别注意在拷贝构造函数和赋值运算符中正确地处理这些指针,以避免重复释放或内存泄漏的问题。
-
基于继承的析构函数:在继承关系中,派生类的析构函数会自动调用基类的析构函数,以进行基类部分的清理工作。因此,在派生类的析构函数中,不需要显式调用基类的析构函数。
总结来说,析构函数在对象的生命周期结束时自动调用,用于进行对象的清理工作。需要注意手动管理动态分配的资源、正确处理指针成员变量以及继承关系中的析构函数调用。正确地编写和使用析构函数,可以避免内存泄漏和资源泄漏等问题。
在使用C++析构函数时,有一些需要注意的事项:
-
析构函数的访问权限:与其他成员函数一样,析构函数可以是公有的、私有的或保护的。通常情况下,析构函数应该是公有的,以便对象的所有者可以调用它进行清理工作。
-
派生类的析构函数:如果存在继承关系,派生类的析构函数会自动调用基类的析构函数。但是,如果基类的析构函数是私有的,派生类的析构函数无法调用它。因此,在继承关系中,基类析构函数通常应该是公有的或保护的。
-
虚析构函数:如果希望基类指针能正确地调用派生类的析构函数,基类中的析构函数应该声明为虚析构函数。这样,在通过基类指针删除派生类对象时,会执行正确的析构函数,以避免内存泄漏。
-
在析构函数中避免抛出异常:析构函数应该尽量避免抛出异常。因为当析构函数抛出异常时,会导致程序的不确定行为。如果需要在析构函数中执行可能会抛出异常的操作,应使用适当的异常处理机制来处理异常,以确保程序的安全性。
-
遵循RAII原则:RAII(资源获取即初始化)是一种C++的编程技术,它通过在构造函数中获取资源,在析构函数中释放资源,以确保资源的正确管理。因此,在设计类时应该遵循RAII原则,将资源的分配和释放操作放在构造函数和析构函数中,以提高代码的可靠性和可维护性。
注意以上事项可以正确地使用C++析构函数,确保对象的资源正确释放,避免内存泄漏和资源泄漏的问题。
C++中的析构函数是一个特殊的成员函数,用于在对象被销毁时执行清理工作。它的名称与类名称相同,前面加上一个波浪号(~)作为前缀,并且没有任何参数。
析构函数在以下情况下被自动调用:
- 对象的生命周期结束,例如对象超出作用域。
- 当delete运算符被用于释放动态分配的内存时。
- 对象是另一个对象的成员,并且该对象的析构函数被调用。
在C++中,析构函数可以被声明为虚函数。虚析构函数在基类中声明,并被派生类继承和重写。当通过基类指针删除派生类对象时,只有基类析构函数是虚函数,才会调用正确的派生类析构函数。
使用虚析构函数的好处是,在删除基类指针时,可以确保正确调用派生类的析构函数,从而确保正确释放资源,防止内存泄漏。
8> 重载
C++中的函数重载是指在同一个作用域内,可以定义多个具有相同名称但参数列表不同的函数。其实现方式如下:
1. 函数名相同,但参数列表不同:重载函数的函数名必须相同,但是参数列表不同,包括参数的个数、类型、顺序等。例如:
void print(int num);
void print(double num);
1.函数返回类型可以相同也可以不同:函数的返回类型可以相同,也可以不同。返回类型不是函数重载的决定因素,只有在参数列表不同时才会进行重载。
2.函数重载的解析:在调用重载函数时,编译器会根据函数的参数列表来决定调用哪个函数。如果没有找到完全匹配的函数,编译器将尝试进行类型转换来匹配其他重载函数。如果找到多个匹配的函数,编译器将选择最匹配的函数进行调用。
注意事项:
- 与函数的返回类型无关,只与函数的参数列表有关。
- 重载函数可以有不同的实现逻辑,但函数的行为和功能应该相似。
- 函数的重载只能通过参数列表来进行区分,不能通过返回类型来进行区分。
下面是一个示例代码:
#include <iostream>
void print(int num) {
std::cout << "Integer: " << num << std::endl;
}
void print(double num) {
std::cout << "Double: " << num << std::endl;
}
int main() {
print(5); // 调用 print(int)
print(3.14); // 调用 print(double)
return 0;
}
输出结果为:
Integer: 5
Double: 3.14
9> 操作符重载
C++中的操作符重载是指通过重新定义操作符的含义和行为来扩展对自定义类型的操作。通过操作符重载,可以使自定义类型的对象可以像内置类型一样使用各种操作符进行操作。
操作符重载的实现方式如下:
返回类型 operator 操作符(参数列表);
1.操作符重载的返回类型:操作符重载的返回类型和操作符的语义相关,可以是任何合法的返回类型。
2.操作符重载的参数列表:操作符重载的参数列表是根据操作符的特性来决定的,可以是零个、一个或多个参数。注意事项:
- 不是所有的操作符都可以被重载,只有部分操作符可以被重载。
- 操作符重载函数可以是成员函数或非成员函数,具体选择取决于操作符的操作数。
下面是一个示例代码,演示了如何重载加法操作符:
#include <iostream>
class Complex {
private:
double real;
double imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
// 成员函数形式的操作符重载
Complex operator+(const Complex& other) const {
double r = real + other.real;
double i = imag + other.imag;
return Complex(r, i);
}
};
int main() {
Complex c1(1.0, 2.0);
Complex c2(2.0, 3.0);
Complex c3 = c1 + c2;
std::cout << "c3.real = " << c3.real << ", c3.imag = " << c3.imag << std::endl;
return 0;
}
输出结果为:
c3.real = 3, c3.imag = 5
在这个示例中,我们通过重载+
操作符,可以直接对两个Complex
对象进行相加运算。
2.virtual的使用
1> 虚函数
在C++中,virtual
是一个关键字,用于声明一个类的成员函数为虚函数。虚函数是面向对象编程中的一个重要概念,它允许在派生类中重新定义基类的同名函数,从而实现多态性。
当一个基类的成员函数被声明为虚函数时,它将允许在派生类中进行函数的重新定义。这样,通过一个基类指针或引用调用虚函数时,实际执行的是派生类中定义的函数,而不是基类中的函数。
为了使一个成员函数成为虚函数,只需要在函数声明前加上 virtual
关键字。例如:
class Animal {
public:
virtual void makeSound() {
cout << "Animal makes sound" << endl;
}
};
在上面的例子中,makeSound()
函数被声明为虚函数。这意味着在派生类中可以重新定义 makeSound()
函数。
派生类中可以通过使用 override
关键字来重新定义基类的虚函数。例如:
class Dog : public Animal {
public:
void makeSound() override {
cout << "Dog barks" << endl;
}
};
在上面的例子中,Dog
类继承自 Animal
类,并重新定义了 makeSound()
函数,用来表示狗叫的声音。
当通过基类指针或引用调用虚函数时,会根据指针或引用的实际类型来决定执行哪个类的函数。例如:
Animal* animal = new Dog();
animal->makeSound(); // 输出 "Dog barks"
在上面的例子中,animal
是一个指向 Animal
类对象的指针,但它实际指向的是一个 Dog
类对象。因此,调用 makeSound()
函数时,执行的是 Dog
类中重新定义的函数。
总结一下,virtual
关键字用于声明一个成员函数为虚函数,使得它可以在派生类中被重新定义。这样可以实现多态性,通过基类指针或引用调用虚函数时,根据指针或引用的实际类型来决定执行哪个类的函数。
2> 虚函数列表
在C++中,虚函数列表是指一个类中的虚函数的集合。通过将函数声明为虚函数,可以实现多态性和动态绑定。虚函数列表用于存储类的虚函数的地址,以便在运行时根据对象的实际类型调用正确的函数。
以下是一个示例类的虚函数列表:
class Shape {
public:
virtual void draw() = 0;
virtual double area() = 0;
};
class Circle : public Shape {
public:
void draw() override {
// 绘制圆形
}
double area() override {
// 计算圆形的面积
}
};
class Rectangle : public Shape {
public:
void draw() override {
// 绘制矩形
}
double area() override {
// 计算矩形的面积
}
};
int main() {
Shape* shape;
Circle circle;
Rectangle rectangle;
shape = &circle;
shape->draw(); // 调用Circle的draw()函数
shape = &rectangle;
shape->draw(); // 调用Rectangle的draw()函数
return 0;
}
在上面的示例中,Shape类是一个抽象基类,它包含纯虚函数draw()和area(),这些函数在派生类中必须被实现。Circle和Rectangle类都是Shape类的派生类,它们重写了draw()和area()函数。在main函数中,通过将Shape指针指向Circle和Rectangle对象,可以根据对象的实际类型调用正确的draw()函数。这就是虚函数列表的作用。
C++中的虚函数列表是在类的虚函数表中存储的。虚函数表是一个特殊的数据结构,用于存储类的虚函数的地址。每个包含虚函数的类都有一个虚函数表,该表在运行时根据对象的实际类型进行查找,以调用正确的虚函数。
虚函数表的表现形式可以通过编译器的实现而有所不同,但通常具有以下特点:
- 虚函数表是一个数组,其中每个元素是一个指向虚函数的指针。
- 虚函数表中的每个元素对应一个虚函数,在表中的顺序与类定义中声明的虚函数的顺序相对应。
- 每个类只有一个虚函数表,该表存储在静态内存中,即在程序运行时只有一个副本。
- 虚函数表位于类的对象布局中的一个特定位置,通常是对象的开始位置或者是对象的虚指针(vptr)的位置。
- 每个对象都有一个指向其类的虚函数表的指针,该指针被称为虚指针(vptr)。
- 虚指针在对象的构造函数中被初始化,指向类的虚函数表;在对象的析构函数中被清除。
- 当通过指向基类的指针或引用调用虚函数时,编译器将使用对象的虚指针来查找正确的虚函数,并调用该函数。
需要注意的是,虚函数表的具体实现和细节是由编译器决定的,并且可能会因编译器的不同而有所区别。以上是一种常见的虚函数表的表现形式,但并不是所有的编译器都采用相同的实现方式。
3> 重写
在C++中,重写(override)是指子类中重新定义基类中已有的成员函数的过程。重写与重载(overload)不同,重写是在继承关系中发生的,而重载是在同一个类中的不同成员函数中发生的。重写的规则是,子类中的重写函数必须与基类中的原函数有相同的函数名、相同的参数列表和相同的返回类型。
下面是一个重写的例子:
#include <iostream>
using namespace std;
// 基类
class Animal {
public:
virtual void sound() {
cout << "Animal makes a sound" << endl;
}
};
// 子类
class Dog : public Animal {
public:
void sound() {
cout << "Dog barks" << endl;
}
};
int main() {
Animal animal;
Dog dog;
animal.sound(); // 输出: Animal makes a sound
dog.sound(); // 输出: Dog barks
return 0;
}
在上面的例子中,Animal类有一个名为sound()
的成员函数,子类Dog重写了这个函数。在主函数中创建了一个Animal对象和一个Dog对象,并分别调用它们的sound()
函数。由于Dog类重写了sound()
函数,所以Dog对象调用sound()
函数时输出的是"Dog barks",而Animal对象调用sound()
函数时输出的是"Animal makes a sound"。这就是重写的效果。
4> 多态
C++中的多态是指在面向对象编程中,通过使用基类的指针或引用来调用派生类的同名虚函数,实现对不同对象的统一操作。
多态的实现依赖于虚函数。在C++中,通过将基类函数声明为虚函数并使用关键字"virtual"来实现多态。在基类中,如果一个成员函数被声明为虚函数,则派生类可以重写该函数并以不同的实现方式来覆盖基类的函数。
当通过基类的指针或引用来调用虚函数时,编译器会根据实际指向的对象类型来确定调用的是哪个版本的虚函数,这就实现了多态。通过多态,可以实现对不同对象的统一操作,同时可以避免在编译时确定具体对象类型而造成的代码冗余。
例如,假设有一个基类Animal,派生类Dog和Cat都继承自Animal,并且都重写了虚函数"makeSound"。当通过Animal的指针或引用调用makeSound函数时,可以根据指向的具体对象类型来调用相应的函数版本,即使Animal指针指向的是Dog或Cat对象。
#include<iostream>
class Animal {
public:
virtual void makeSound() {
std::cout << "Animal makes a sound." << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Dog barks." << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "Cat meows." << std::endl;
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound(); // Output: Dog barks.
animal2->makeSound(); // Output: Cat meows.
delete animal1;
delete animal2;
return 0;
}
在上面的例子中,通过Animal指针调用makeSound函数时,实际上调用的是指向的具体对象(Dog或Cat)的makeSound函数版本,这就体现了C++中的多态性。
5> 纯虚函数
C++中纯虚函数(pure virtual function)是在基类中声明的虚函数,它在基类中没有实现,而是由派生类进行实现。纯虚函数的声明形式为:virtual returnType functionName(parameters) = 0;
,其中= 0
表示该函数是纯虚函数。
基类中的纯虚函数用于定义接口,派生类必须实现基类中的纯虚函数,否则派生类也成为抽象类,无法实例化对象。
以下是一个示例,展示了纯虚函数的用法:
#include <iostream>
// 基类Shape
class Shape {
public:
// 纯虚函数,定义了Shape的接口
virtual void draw() = 0;
// 普通虚函数
virtual void show() {
std::cout << "This is a Shape." << std::endl;
}
};
// 派生类Circle
class Circle : public Shape {
public:
// 实现基类的纯虚函数draw
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};
// 派生类Rectangle
class Rectangle : public Shape {
public:
// 实现基类的纯虚函数draw
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};
int main() {
// 无法实例化Shape对象,因为Shape是抽象类
//Shape shape;
Circle circle;
circle.draw();
circle.show();
Rectangle rectangle;
rectangle.draw();
rectangle.show();
return 0;
}
输出结果为:
Drawing a circle.
This is a Shape.
Drawing a rectangle.
This is a Shape.
在上述示例中,Shape是一个抽象类,其中draw()
函数是纯虚函数,定义了Shape的接口。派生类Circle和Rectangle必须实现draw()
函数,否则它们也成为抽象类。通过实例化Circle和Rectangle对象,可以调用其实现的draw()
函数和基类Shape的show()
函数。
5> 抽象类
C++抽象类是一种特殊类型的类,无法实例化对象。抽象类常用于作为其他类的基类,定义了一些基本的属性和方法,供派生类继承和实现。抽象类中可以包含纯虚函数,即没有具体实现的函数,由派生类来实现。抽象类中可以有普通成员函数和数据成员。
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // 声明纯虚函数
void normalFunction() {
// 普通函数实现
}
};
抽象类无法直接实例化,只能作为基类使用。派生类必须实现抽象类中的纯虚函数,否则派生类也将成为抽象类。例如:
class DerivedClass : public AbstractClass {
public:
void pureVirtualFunction() {
// 派生类实现纯虚函数
}
};
在派生类中,必须实现抽象类中的纯虚函数,否则编译器会报错。派生类可以选择性地实现抽象类中的普通函数。
使用抽象类的好处是,它可以定义一些基本的行为和接口,但不需要实现具体细节。这样可以在派生类中自由实现这些接口,同时保持一致的接口定义。抽象类也允许多态性的应用,可以通过指向抽象类的指针或引用来操作派生类的对象。
3.继承
1> 概念
C++中的继承是一种面向对象编程的重要特性,它允许派生类(子类)从基类(父类)中继承成员变量和成员函数,以及添加自己的成员变量和成员函数。
继承的基本语法为:
class DerivedClass : [访问控制符] BaseClass {
// 添加派生类的成员变量和成员函数
};
在继承中,被继承的类称为基类或父类,继承这个类的类称为派生类或子类。继承可以分为三种访问控制符:
- public继承:基类的public和protected成员在派生类中保持它们的访问性不变,而private成员在派生类中是不可访问的。
- protected继承:基类的public和protected成员在派生类中都变为protected成员,而private成员在派生类中是不可访问的。
- private继承:基类的public和protected成员在派生类中都变为private成员,而private成员在派生类中是不可访问的。
继承关系中,派生类可以访问基类的成员变量和成员函数,而无需重新定义,即可以直接使用。如果派生类需要对基类的成员变量和成员函数进行修改或扩展,可以进行重写或覆盖。
C++中的继承还有几个重要的概念:
- 单继承:一个派生类只能继承一个基类。
- 多继承:一个派生类可以同时继承多个基类。
- 虚继承:用于解决多继承中的二义性问题,通过关键字"virtual"来声明虚继承。
- 继承权限:访问控制符可以控制派生类对基类成员的访问权限。
下面是一个简单的例子,演示了C++中的继承:
#include<iostream>
class Shape {
protected:
int width;
int height;
public:
Shape(int w, int h) : width(w), height(h) {}
void setWidth(int w) {
width = w;
}
void setHeight(int h) {
height = h;
}
virtual int getArea() {
return 0;
}
};
class Rectangle : public Shape {
public:
Rectangle(int w, int h) : Shape(w, h) {}
int getArea() override {
return width * height;
}
};
class Triangle : public Shape {
public:
Triangle(int w, int h) : Shape(w, h) {}
int getArea() override {
return (width * height) / 2;
}
};
int main() {
Rectangle rect(5, 3);
Triangle tri(4, 6);
std::cout << "Rectangle area: " << rect.getArea() << std::endl; // Output: 15
std::cout << "Triangle area: " << tri.getArea() << std::endl; // Output: 12
return 0;
}
在上面的例子中,Shape是基类,Rectangle和Triangle是派生类。派生类继承了基类的成员变量和成员函数,并添加了自己的成员函数。派生类可以通过调用基类的成员函数来设置和访问基类的成员变量,并可以通过重写虚函数来实现自己的逻辑。