权限控制

类在设计时,可以把属性和行为放在不同的权限下,加以控制。三种访问权限:

public(公共权限),protected(保护权限),private(私有权限)

C++中封装的声明语法示例:

class MyClass {
private:
    int privateVar;  // 私有成员,只能在类的内部访问

protected:
    int protectedVar;  // 受保护成员,可以被子类访问,但外部不能直接访问

public:
    int publicVar;  // 公有成员,类的外部也可以访问

    // 构造函数
    MyClass(int pVar, int protVar, int pubVar) {
        privateVar = pVar;
        protectedVar = protVar;
        publicVar = pubVar;
    }

    // 公有成员函数
    void showPrivateVar() {
        cout << "Private Variable: " << privateVar << endl;
    }

    void setPrivateVar(int newValue) {
        privateVar = newValue;
    }
};

访问示例

int main() {
    MyClass obj(1, 2, 3);

    // 访问公有成员
    cout << "Public Variable: " << obj.publicVar << endl;

    // 通过公有方法访问私有成员
    obj.showPrivateVar();

    // 无法直接访问私有或受保护成员
    // obj.privateVar = 5;  // 错误:privateVar 是私有的
    // obj.protectedVar = 5; // 错误:protectedVar 是受保护的

    return 0;
}

衍生

struct和class的区别

  1. 访问权限

    • struct: 默认情况下,所有成员(变量和函数)都是 public

    • class:默认情况下,所有成员(变量和函数)都是 private

  2. 使用场景

    • struct:通常用于表示简单的数据结构,主要用于数据的存储和传递,虽然可以向class一样定义成员函数和构造函数,但一般用于存储数据成员
    • class:通常用于表示更复杂的对象,包含数据和行为,class是面向对象编程的中的核心概念,通常用于定义更复杂逻辑的类型,包括继承、多态等特性

struct示例

#include <iostream>
using namespace std;
struct MyStruct {
    int x;
    void show() {
        cout << "x = " << x << endl;
    }
};
int main() {
    MyStruct s;
    s.x = 10;  // 默认是 public,可以直接访问
    s.show();  // 输出:x = 10
    return 0;
}

class示例

#include <iostream>
using namespace std;
class MyClass {
    int x;  // 默认是 private,外部无法直接访问
public:
    void setX(int val) {
        x = val;
    }
    void show() {
        cout << "x = " << x << endl;
    }
};
int main() {
    MyClass c;
    // c.x = 10;  // 错误:x 是私有的,不能直接访问
    c.setX(10);  // 必须通过公有函数设置值
    c.show();    // 输出:x = 10
    return 0;
}

友元

友元(friend)是C++中的一个关键字,它允许某些特定的函数或者类访问另一个类的私有(private)和受保护的(protected)成员,这些成员一般只能被类的成员函数直接访问

在C++中,友元可以是函数或类

  1. 友元函数

一个普通函数(可以是全局函数也可以是成员函数)可以被声明为另一个类的友元,这样该函数就可以访问该类的私有和受保护成员。友元函数不是类的成员函数,但它可以访问该类的所有成员。

全局函数做友元示例:

#include <iostream>
using namespace std;
class MyClass {
private:
    int secret;

public:
    MyClass() : secret(42) {}

    // 声明 friend 函数
    friend void showSecret(const MyClass &obj);
};

// 定义 friend 函数
void showSecret(const MyClass &obj) {
    // 友元函数可以访问私有成员
    cout << "The secret is: " << obj.secret << endl;
}

int main() {
    MyClass obj;
    showSecret(obj);  // 调用友元函数
    return 0;
}

成员函数做友元示例:

#include <iostream>
using namespace std;

class Building;
class goodGay
{
public:
    goodGay();

    void visit();  // 让visit函数可以访问Building中私有成员
    void visit2(); // 让visit函数不可以访问Building中私有成员

    Building *building;
};

class Building
{
    // 告诉编译器 goodGay类下的visit成员函数作为本类的好朋友,可以访问私有成员
    friend void goodGay::visit();

public:
    Building();
    string m_SittingRoom;

private:
    string m_BedRoom;
};

// 类外实现成员函数
Building::Building()
{
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
}

goodGay::goodGay()
{
    building = new Building;
}
void goodGay::visit()
{
    cout << "visit函数正在访问:" << building->m_SittingRoom << endl;
    cout << "visit函数正在访问:" << building->m_BedRoom << endl;
}
void goodGay::visit2()
{
    cout << "visit2函数正在访问:" << building->m_SittingRoom << endl;
    //cout << "visit2函数正在访问:" << building->m_BedRoom << endl;//无法访问
}
void test01()
{
    goodGay gg;
    gg.visit();
    gg.visit2();
}
int main()
{
    test01();
    system("pause");
    return 0;
}
  1. 类做友元

一个类也可以被声明为另一个类的友元,这样这个类的所有成员函数都可以访问另一个类的私有和受保护成员。

类做友元示例

#include <iostream>
using namespace std;
class MyClass {
private:
    int secret;

public:
    MyClass() : secret(42) {}

    // 声明 FriendClass 为友元类
    friend class FriendClass;
};

class FriendClass {
public:
    void showSecret(const MyClass &obj) {
        // FriendClass 的成员函数可以访问 MyClass 的私有成员
        cout << "The secret is: " << obj.secret << endl;
    }
};

int main() {
    MyClass obj;
    FriendClass friendObj;
    friendObj.showSecret(obj);  // 调用 FriendClass 的成员函数访问私有数据
    return 0;
}

继承

继承允许一个类从另一个类获得属性(成员变量)和方法(成员函数)。继承使得代码可以重用、拓展和组织更加简洁。

在C++中,继承是通过类之间的层次关系实现的,一个子类(派生类,derivedclass)继承一个父类(基类,baseclass),从而可以访问和使用父类的功能。继承通过关键字publicprotectedprivate来控制继承的访问权限。

继承的语法

C++中,继承的基本语法如下:

class BaseClass {
    // 基类的成员
};

class DerivedClass : public BaseClass {
    // 子类的成员
};

这里DerivedClass继承了BaseClassDerivedClass可以访问和使用BaseClass中的非私有成员。


继承类型

  • 公有继承

    1. 基类的public成员在派生类中仍然是public

    2. 基类的protected成员在派生类中仍然是protected

    3. 基类的private成员在派生类中是不可访问的,但是可以通过基类的protected成员间接访问。

  • 保护继承

    1. 基类的publicprotected成员在派生类中变为protected
    2. 基类的private成员不可直接访问。
  • 私有继承

    1. 基类的 publicprotected 成员在派生类中变为 private
    2. 基类的 private 成员不可直接访问。

代码示例:

#include <iostream>
using namespace std;

class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};

class Derived : public Base {
public:
    void accessBaseMembers() {
        publicVar = 10;        // 可以访问公有成员
        protectedVar = 20;     // 可以访问受保护成员
        // privateVar = 30;    // 无法访问私有成员,编译错误
    }
};

int main() {
    Derived obj;
    obj.publicVar = 100;       // 可以访问公有成员
    // obj.protectedVar = 200; // 无法访问受保护成员,编译错误
    return 0;
}

在这个例子中,Derived 类继承了 Base 类。在派生类 Derived 中,我们可以访问基类的 publicprotected 成员,但不能访问基类的 private 成员。main 函数中只能访问 publicVar,因为它是公有继承的。


衍生

C++ 支持多重继承,但这会引发一些复杂的情况。一个典型的问题是菱形继承问题

当两个派生类同时继承一个基类,某个类同时继承这两个派生类,形成一个菱形结构,这种继承被称为菱形继承钻石继承

菱形继承

  1. 二义性问题

当类通过菱形继承路径继承一个共同的基类时,基类的成员会被重复继承两次,这样派生类就会有两份基类成员的副本,造成访问时的歧义,无法明确到底使用哪个基类的成员。即发生二义性问题。

代码示例:

#include <iostream>
using namespace std;

class A {
public:
    int value;
};

class B : public A {};
class C : public A {};

class D : public B, public C {};

int main() {
    D obj;
    obj.value = 10;  // 这行代码会导致二义性错误
    return 0;
}

在这个例子中,类 D 通过 BC 继承了 A 的两份副本。因此,当我们试图访问 obj.value 时,编译器不知道应该使用 B 继承的 A::value,还是 C 继承的 A::value,从而产生了二义性

  1. 二义性问题解决
  • 使用作用域对数据进行区分

在菱形继承的情况下,使用作用域解析符可以暂时解决二义性问题。你可以明确指定你要访问哪一个基类的成员。但这只是一种临时的解决办法,仍然存在重复的成员副本,并不是最佳的解决方法。

代码示例:

#include <iostream>
using namespace std;

class A {
public:
    int value;
};

class B : public A {};
class C : public A {};

class D : public B, public C {};

int main() {
    D obj;
    obj.B::value = 10;  // 指定访问 B 中继承的 A::value
    obj.C::value = 20;  // 指定访问 C 中继承的 A::value

    cout << "B::value = " << obj.B::value << endl;
    cout << "C::value = " << obj.C::value << endl;
    return 0;
}

在这个例子中,我们使用作用域解析符 B::valueC::value 来区分访问不同的 A::value 副本以此来解决二义性问题。但这会带来复杂性,也会增加维护和理解代码的难度,同时造成资源浪费。

  • 虚继承解决二义性问题

当我们通过虚继承某个类时,编译器保证这个基类的成员在继承链中只保留一份,而不会在派生类中出现多次副本,也就是说,虚继承可以使得基类成员在多重继承的结构中不会重复继承。

在没有虚继承的情况下,派生类中的每个继承路径会独立地继承积累的成员,因此会产生多个副本。虚继承的本质不是简单的通过作用域来区分不同的成员,而是通过编译器内部的机制来确保基类成员只有一份共享的副本。

在使用虚继承的情况下,编译器会确保基类成员只存在一份共享的副本,并且所有继承路径都指向同一个基类实例。

代码示例:

#include <iostream>
using namespace std;

class A {
public:
    int value;
};

class B : public virtual A {};
class C : public virtual A {};

class D : public B, public C {};

int main() {
    D obj;
    obj.value = 10;  // 不再有二义性
    cout << obj.value << endl;
    return 0;
}

在这个例子中,BC 虚继承了 A,这样当 D 继承 BC 时,A 的成员 value 只会有一个副本。访问 obj.value 时,编译器知道只有一份 A::value 存在,因此没有歧义。


多态

多态允许不同类型的对象通过相同的接口进行交互。在C++中,多态的核心思想是让一个 函数或方程能够根据对象类型的不同,表现出不同的行为。

多态的类型

  • 编译时多态(静态多态):通过函数重载模板来实现。
  • 编译时多态(动态多态):通过虚函数继承来实现。

多态的条件

要是实现运行时多态,需要满足以下条件:

  1. 继承:派生类必须继承自基类。
  2. 虚函数:基类中的方法必须声明为virtual,如此一来子类可以重写该方法。
  3. 基类指针或引用:通过基类的指针或引用指向派生类对象。

实现编译时多态,满足条件如下:

  1. 函数重载:函数重载允许你在同一个作用域内定义多个同名但参数不同(参数的类型或数量不同)的函数,编译器会根据传递的参数在编译时决定调用哪个函数
  2. 运算符重载:运算符重载允许你重定义 C++ 内置运算符(如 +, -, * 等)的行为(使用operator关键字),使它们能够处理自定义类型。
  3. 模板:模板(使用template关键字定义模板)允许你编写通用代码,编译器在编译时根据传入的类型实例化不同的函数或类。模板可以用于函数,类,和别名(如typedefusing).

模板代码示例:

template <typename T>
T add(T a, T b) {
    return a + b;
}

在这个例子中,add 函数是一个模板函数,可以接受任何类型的参数(只要该类型支持 + 运算符)。

静态多态的条件不需要同时满足,具体的实现方式取决于你选择哪种静态多态的技术。它们是独立的,可以单独使用,也可以结合使用。

动态多态的条件一般要同时满足,动态多态的实现依赖于继承、虚函数以及基类指针或引用,缺少任何一个条件都会导致动态多态无法正常工作。


编译时多态

编译时多态是指在编译期间决定调用哪个函数。典型的方式的函数重载运算符重载

函数重载

函数重载允许多个同名函数根据参数类型或数量的不同来区分 ,编译器根据传递的参数在编译时决定调用哪个函数。

#include <iostream>
using namespace std;

class Print {
public:
    void show(int i) {
        cout << "Integer: " << i << endl;
    }

    void show(double d) {
        cout << "Double: " << d << endl;
    }
};

int main() {
    Print p;
    p.show(10);     // 调用 show(int)
    p.show(5.5);    // 调用 show(double)
    return 0;
}

在这个例子中,show函数被重载以支持不同的数据类型,编译器会在编译时决定调用哪个函数。


运行时多态

运行时多态是指在程序运行意见根据对象的类型动态决定调用哪个函数。它通常通过继承和虚函数实现。虚函数允许子类重写基类的方法,而当我们使用基类指针或引用时,实际调用的是子类的重写版本。

虚函数和多态

虚函数是实现运行时多态的关键。通过在基类中使用virtual关键字定义函数,子类可以重写该函数。当我们使用基类指针或引用调用这个虚函数时,会根据实际对象的类型调用相应的子类版本。

代码示例:

#include <iostream>
using namespace std;

// 基类
class Animal {
public:
    virtual void speak() {
        cout << "Animal speaks" << endl;
    }
};
// 派生类
class Dog : public Animal {
public:
    void speak() override {
        cout << "Dog barks" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout << "Cat meows" << endl;
    }
};

int main() {
    Animal* animal;
    Dog dog;
    Cat cat;

    // 使用基类指针指向派生类对象
    animal = &dog;
    animal->speak();  // 调用 Dog 的 speak

    animal = &cat;
    animal->speak();  // 调用 Cat 的 speak

    return 0;
}

在这个例子中,基类 Animalspeak 函数是虚函数。当基类指针 animal 指向 Dog 对象时,调用的是 Dogspeak 函数;当指向 Cat 对象时,调用的是 Catspeak 函数。这就是运行时多态,因为在运行时根据实际对象的类型调用对应的函数。


衍生

虚函数和虚函数表(vtbl)

在实现运行时多态的过程中,C++ 使用了一个称为**虚表(V-Table)**的机制。每个类都有一个虚表,用于存储虚函数的地址。当调用虚函数时,程序根据实际对象的类型查找虚表中的函数地址,然后调用对应的函数。

image-20240901143905617

  • 虚表(V-Table):

虚表(V-Table)是编译器为支持虚函数机制生成的一个隐式的数据结构。每个类都有一张虚表,表中存储该类的虚函数指针。虚表的存在使得在运行时能够根据对象的实际类型找到并调用正确的虚函数。

每个定义了虚函数的类都会有一张虚函数表,表中记录了该类的虚函数的指针,当派生类重写基类的虚函数时,派生类的虚表中的相应位置将指向派生类的重写函数。

  • 虚指针(V-Ptr):每个对象中都有一个隐藏的虚指针,指向该类的虚表。该指针在对象创建时被初始化,指向该对象所属类的虚表。这样,编译器在运行时可以通过虚指针找到正确的虚函数实现。

虚函数表的调用过程

当通过基类指针或引用调用虚函数时,程序会执行以下步骤:

  1. 查找虚指针(V-Ptr):每个对象都持有一个指向其所属类虚表的指针(虚指针)。当通过基类指针或引用调用虚函数时,程序首先通过虚指针找到虚表。
  2. 查找虚表中的函数指针:根据虚函数的位置,在虚表中找到对应的函数指针。每个虚表保存了该类所有虚函数的地址。
  3. 调用实际的函数:通过从虚表中获取的函数指针,程序调用对应的派生类版本的虚函数。

代码示例:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void display() {  // 虚函数
        cout << "Base display" << endl;
    }
    virtual ~Base() {}  // 虚析构函数
};

class Derived : public Base {
public:
    void display() override {  // 重写虚函数
        cout << "Derived display" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();  // 基类指针指向派生类对象
    basePtr->display();  // 调用 Derived 类的 display()

    delete basePtr;  // 使用虚析构函数销毁 Derived 对象
    return 0;
}

调用过程:

  1. basePtr->display() 被调用时,程序通过 basePtr 对象的虚指针找到虚表。
  2. 在虚表中查找 display() 函数的指针。因为 basePtr 实际指向的是 Derived 对象,虚表指向的是 Derived::display()
  3. 程序最终调用的是 Derived::display(),实现了运行时多态。

纯虚函数和抽象类

在 C++ 中,如果基类的某个函数不需要提供具体实现,只需要子类去实现它,我们可以将这个函数声明为纯虚函数。含有纯虚函数的类称为抽象类,它不能被实例化。

纯虚函数语法代码示例:

class Base {
public:
    virtual void func() = 0;  // 纯虚函数
};

纯虚函数的目的是强制派生类必须实现这个函数。代码示例如下:

#include <iostream>
using namespace std;

class Animal {
public:
    // 纯虚函数
    virtual void speak() = 0;
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "Dog barks" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout << "Cat meows" << endl;
    }
};

int main() {
    Animal* animal;

    Dog dog;
    Cat cat;

    animal = &dog;
    animal->speak();  // 调用 Dog 的 speak

    animal = &cat;
    animal->speak();  // 调用 Cat 的 speak

    return 0;
}

在这个例子中,Animal 是一个抽象类,不能被实例化。但它可以作为基类,DogCat 都必须实现 speak 这个纯虚函数。