在 C++ 中,类和对象是面向对象编程(OOP)的核心概念。类是一个用户定义的数据类型,它封装了数据和对数据的操作。对象则是类的实例。

类和对象

类的定义

  定义一个类需要使用关键字classs,然后指定类的名称,并且类的猪蹄是包含在一对花括号中,主体包含类的成员变量和成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

class Car {
public:
// 公有成员函数
void setBrand(std::string b) {
brand = b;
}

void setModel(std::string m) {
model = m;
}

void displayInfo() {
std::cout << "Brand: " << brand << ", Model: " << model << std::endl;
}

private:
// 私有成员变量
std::string brand;
std::string model;
};

对象的创建

创建一个类的对象并使用它的方法:

1
2
3
4
5
6
7
8
9
10
int main() {
Car myCar; // 创建对象

myCar.setBrand("Toyota");
myCar.setModel("Corolla");

myCar.displayInfo(); // 输出 Brand: Toyota, Model: Corolla

return 0;
}

类访问修饰符

  关键字 public、private、protected称为访问修饰符。
  一个类可以有多个public、protected 或 private 标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。成员和类的默认访问修饰符是 private

  • public 公有成员
    1. 公有成员在程序中类的外部是可访问的。可以不使用任何成员函数来设置和获取公有变量的值。
    2. 公有成员通常是类的接口的一部分。
  • private私有成员:
    1. 私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类的成员函数和友元函数可以访问私有成员
      1. 默认情况下,类的所有成员都是私有的。
    2. 实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数。
  • protected受保护成员:
    1. protected(受保护)成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。
    2. 保护成员适用于需要在继承结构中共享但不希望被外部代码访问的数据。

指向类的指针

指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。与所有的指针一样,您必须在使用指针之前,对指针进行初始化。
在 C++ 中,指向类的指针指向一个类的对象,与普通的指针相似,指向类的指针可以用于访问对象的成员变量和成员函数。
指向类的指针还可以用于动态分配内存,创建类的对象。
指向类的指针可以作为函数参数传递。

this指针

  • 在 C++ 中,this 指针是一个特殊的指针,它指向当前对象的实例。
  • 在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。
  • this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。
  • 当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针。
  • 友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。

理解:
当我们调用成员函数时,实际上是替某个对象调用它。
成员函数通过一个名为 this 的额外隐式参数来访问调用它的那个对象,当我们调用一个成员函数时,用请求该函数的对象地址初始化 this

当你进入一个房子后,
你可以看见桌子、椅子、地板等,
但是房子你是看不到全貌了。
对于一个类的实例来说,
你可以看到它的成员函数、成员变量,
但是实例本身呢?
this是一个指针,
它时时刻刻指向你这个实例本身。

类的静态成员

可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。
静态成员函数:如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
静态成员函数有一个类范围,他们不能访问类的 this 指针。

类的常量成员:

类的常量成员 通常是指被声明为 const 类型的类成员变量。const 成员变量的值一旦被初始化,就不能再修改。类的常量成员变量有助于确保类的某些数据成员在对象的生命周期内保持不变。

  • 由于常量成员变量不能修改,所以必须在构造函数的初始化列表中进行初始化。
    1
    2
    3
    4
    5
    6
    class ClassName {
    private:
    const int constantValue; // 常量成员变量
    public:
    ClassName(int val) : constantValue(val) {} // 初始化常量成员变量
    };
  • 常量静态成员变量
    类的静态常量成员变量也是常见的做法,尤其是在你希望为所有对象共享一个常量时。静态成员变量属于类本身,而不是类的某个特定对象。静态常量成员变量常常用于定义类的一些不变的全局常量,例如数学常数或最大值。
    1
    2
    3
    4
    5
    6
    class ClassName {
    private:
    static const int constantValue = 10; // 静态常量成员变量
    public:
    static int getConstantValue() { return constantValue; }
    };
  • mutable 关键字与常量成员变量的关系
    mutable 关键字使得类的某个成员变量可以在常量成员函数中修改。虽然常量成员函数承诺不修改对象的状态,但如果某个成员变量是 mutable,它就可以在常量成员函数中被修改。这对于一些需要在不改变对象外部状态的情况下进行内部更新的场景非常有用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #include <iostream>
    using namespace std;

    class MyClass {
    private:
    mutable int cache; // 可以在 const 函数中修改

    public:
    MyClass() : cache(0) {}

    // 常量成员函数,可以修改 mutable 成员变量
    void updateCache() const {
    cache++; // 即使是常量函数,也可以修改 mutable 成员
    }

    void showCache() const {
    cout << "Cache: " << cache << endl;
    }
    };

    int main() {
    MyClass obj;
    obj.showCache(); // 输出 Cache: 0

    obj.updateCache(); // 更新 cache
    obj.showCache(); // 输出 Cache: 1
    return 0;
    }
  • 常量成员函数与常量成员变量
    常量成员变量通常与常量成员函数一起使用,以确保对象的状态在函数调用期间保持不变。常量成员函数通常用于访问这些常量成员变量,而不会修改它们。

抽象类

在 C++ 中,抽象类 是一种不能直接实例化的类,通常用于定义接口或者基础类。抽象类通过包含纯虚函数(pure virtual function)来实现,目的是让派生类提供具体实现。
抽象类特点

  • 不能实例化:因为抽象类的纯虚函数没有实现。
  • 可以有构造函数和析构函数:尽管不能实例化,抽象类可以用于构造派生类对象时初始化其基类部分。
  • 可以包含普通成员函数:抽象类可以实现部分功能,让派生类复用。
  • 派生类必须实现所有纯虚函数:除非派生类也定义为抽象类。

友元类

在 C++ 中,友元类(friend class) 是允许另一个类访问其私有成员和保护成员的类。通过友元机制,两个类之间可以共享私有数据,增加类之间的协作性,但也需要注意使用的场景,以免破坏封装性。
通过在类的定义中使用 friend class 关键字,可以将另一个类声明为友元类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class B; // 前向声明

class A {
private:
int secretData; // 私有成员

public:
A(int value) : secretData(value) {}

// 声明 B 是 A 的友元类
friend class B;
};

class B {
public:
void accessA(const A& a) {
// 直接访问 A 的私有成员
std::cout << "Accessing A's private data: " << a.secretData << std::endl;
}
};

类的成员函数

在C++中,类的成员函数是定义在类内部的函数,用于描述类对象的行为或操作。它们可以通过对象或类的实例来调用。成员函数分为几类,包括普通成员函数、构造函数、析构函数、静态成员函数、常量成员函数等。

成员函数的定义方式

  1. 在类内定义:定义直接放在类体内,通常是小型函数
  2. 在类外定义:定义放在类体之外,需要使用作用域解析运算符 :: 来指定函数所属的类

成员函数的访问权限

根据关键字 publicprotectedprivate,成员函数的访问权限分为以下三种:

  • public:可以从类外通过对象访问。
  • protected:只能在类内或派生类中访问。
  • private:仅在类内访问。

普通成员函数

普通成员函数是定义在类内部的函数,用于描述类对象的行为或操作。它们可以通过对象或类的实例来调用。

常成员函数

  • 不允许修改成员变量
  • 在函数声明后加 const 关键字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
void display() const { // 常成员函数
std::cout << "Value: " << value << std::endl;
}
};

int main() {
const MyClass obj(10);
obj.display(); // 调用常成员函数
return 0;
}

静态成员函数

  • 使用 static 关键字声明。
  • 不依赖于对象,只能访问静态成员变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass {
private:
static int count;
public:
static void showCount() { // 静态成员函数
std::cout << "Count: " << count << std::endl;
}
};

int MyClass::count = 5;

int main() {
MyClass::showCount(); // 静态成员函数通过类名调用
return 0;
}

构造函数

构造函数是一种特殊的成员函数,用于初始化对象。它的名字与类名相同,并且没有返回值(连 void 都不能写)。构造函数的主要作用是在创建对象时自动执行初始化操作。
构造函数可以重载,即可以定义多个构造函数,每个构造函数的参数列表不同。当创建对象时,根据传递的参数列表选择合适的构造函数来初始化对象。

默认构造函数

不带参数或参数都有默认值的构造函数。

带参数的构造函数

带参数的构造函数可以在创建对象时传递参数,用于初始化对象的成员变量。

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,并将另一个对象的值复制到新对象中。它的参数是一个同类对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyClass {
public:
int value;
MyClass() { // 默认构造函数
std::cout << "默认构造函数被调用!" << std::endl;
}

MyClass(int v) { // 带参数的构造函数
value = v;
std::cout << "带参数构造函数被调用,value = " << value << std::endl;
}
/*MyClass(int v) : value(v) { // 另一种带参数的构造函数写法
std::cout << "带参数构造函数被调用,value = " << value << std::endl;
}*/

MyClass(const MyClass& obj) { // 拷贝构造函数
value = obj.value;
std::cout << "拷贝构造函数被调用!" << std::endl;
}

};

int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2(10); // 调用带参数的构造函数
MyClass obj3 = obj2; // 调用拷贝构造函数
return 0;
}

委托构造函数(C++11 引入)

委托构造函数是一种特殊的构造函数,它可以在一个构造函数中调用另一个构造函数来初始化对象。这样可以减少代码重复,提高代码的可维护性。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
MyClass() : MyClass(0) {} // 委托到另一个构造函数
MyClass(int x) {
std::cout << "x = " << x << std::endl;
}
};

int main() {
MyClass obj; // 调用默认构造函数,实际委托到 MyClass(int x)
return 0;
}

显式构造函数(explicit)

防止隐式类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
explicit MyClass(int x) {
std::cout << "显式构造函数被调用,x = " << x << std::endl;
}
};

int main() {
MyClass obj1(10); // OK,显式调用
// MyClass obj2 = 20; // 错误,禁止隐式转换
return 0;
}

构造函数与初始化列表

使用初始化列表可以更高效地初始化成员变量,尤其是常量或引用成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass {
int a;
const int b;
int& c;
public:
MyClass(int x, int y, int& z) : a(x), b(y), c(z) { // 初始化列表,这是一个带参数的构造函数
std::cout << "初始化列表构造函数被调用" << std::endl;
}
};

int main() {
int val = 10;
MyClass obj(1, 2, val);
return 0;
}

析构函数

析构函数是一种特殊的成员函数,用于在对象销毁时执行清理操作。它的名字与类名相同,前面加上一个波浪号~,并且没有返回值(连 void 都不能写)。析构函数的主要作用是在对象销毁时自动执行清理操作,例如释放动态分配的内存、关闭文件等。

析构函数只能有一个,不能重载。当对象超出作用域或显式调用 delete 操作符时,会自动调用析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
~MyClass() { // 析构函数
std::cout << "析构函数被调用,清理资源!" << std::endl;
}
};

int main() {
MyClass obj; // 创建对象时调用构造函数
// 离开作用域时自动调用析构函数
return 0;
}

虚函数

虚函数是一种特殊的成员函数,用于实现多态性。在基类中声明虚函数时,需要在函数声明前加上 virtual 关键字。在派生类中重写虚函数时,不需要使用 virtual 关键字,但必须与基类中的虚函数具有相同的函数签名(即函数名、参数列表和返回类型)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
virtual void show() {
std::cout << "基类 show 函数" << std::endl;
}
};

class Derived : public Base {
public:
void show() override { // 重写虚函数
std::cout << "派生类 show 函数" << std::endl;
}
};

int main() {
Base* ptr = new Derived();
ptr->show(); // 调用派生类的 show 函数
delete ptr;
return 0;
}

纯虚函数

纯虚函数是一种特殊的虚函数,用于定义接口。在基类中声明纯虚函数时,需要在函数声明前加上 virtual 关键字和 = 0。纯虚函数没有函数体,派生类必须重写纯虚函数,否则派生类也将成为抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {
public:
virtual void show() = 0; // 纯虚函数
};

class Derived : public Base {
public:
void show() override { // 重写纯虚函数
std::cout << "派生类 show 函数" << std::endl;
}
};

int main() {
Base* ptr = new Derived();
ptr->show(); // 调用派生类的 show 函数
delete ptr;
return 0;
}

内联成员函数

使用 inline 关键字提示编译器将函数展开到调用处。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
inline void greet() { // 内联成员函数
std::cout << "你好!" << std::endl;
}
};

int main() {
MyClass obj;
obj.greet();
return 0;
}

友元函数

友元函数是类的非成员函数,可以在类内部声明,但在类外部定义。友元函数可以访问类的私有成员和保护成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
friend void printValue(const MyClass& obj); // 声明友元函数
};

void printValue(const MyClass& obj) { // 定义友元函数
std::cout << "value = " << obj.value << std::endl;
}

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

继承和派生

继承和派生是密切相关的两个概念,从不同的视角来看待父类与子类的关系。继承是从父类(基类)角度描述的,它强调的是子类可以复用父类的成员。换句话说,父类为子类提供了功能,并且这种功能可以被扩展或修改;派生是从子类角度描述的,它强调的是子类是从父类生成的,并在父类的基础上扩展或修改功能。

基本语法

1
2
3
4
5
6
7
8
class 基类名 {
// 基类的成员
};

class 派生类名 : 访问权限 基类名 {
// 派生类的成员
};

访问控制符可以是public、protected、private,分别表示公有继承、保护继承、私有继承。

继承方式

继承方式决定了父类成员在子类中的访问权限,包括公有继承、保护继承、私有继承。

  • public继承:基类的公有成员和保护成员成为派生类的公有成员和保护成员,基类的私有成员派生类不可访问。
  • protected继承:基类的公有成员和保护成员成为派生类的保护成员,基类的私有成员派生类不可访问。
  • private继承:基类的公有成员和保护成员成为派生类的私有成员,基类的私有成员派生类不可访问。

继承与派生中的构造函数

  1. 构造函数的调用顺序
    当创建一个派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。
    这是因为派生类继承了基类的成员,因此派生类对象的创建需要先初始化基类部分。

  2. 基类构造函数的调用方式

    • 如果派生类没有定义构造函数,则编译器会自动生成一个默认的构造函数,该构造函数会自动调用基类的默认构造函数(前提是基类有默认的构造函数)。
    • 如果派生类定义了构造函数:
      • 如果基类有默认构造函数,派生类可以不显式调用基类的构造函数。编译器会自动调用基类的默认构造函数。
      • 如果基类只有带参数的构造函数,派生类必须显式调用基类的构造函数,否则编译器会报错,因为它不知道该如何初始化基类部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
using namespace std;

// 基类
class Animal {
public:
Animal() {
cout << "Animal's constructor called!" << endl;
}

Animal(string name) {
cout << "Animal's parameterized constructor called with name: " << name << endl;
}

void eat() {
cout << "I can eat!" << endl;
}
};

// 派生类
class Dog : public Animal {
public:
Dog() : Animal("Dog") { // 显式调用基类构造函数
cout << "Dog's constructor called!" << endl;
}

void bark() {
cout << "Woof woof!" << endl;
}
};

int main() {
Dog myDog;
myDog.eat();
myDog.bark();
return 0;
}
/*
输出
Animal's parameterized constructor called with name: Dog
Dog's constructor called!
I can eat!
Woof woof!
/*

继承与派生中的析构函数

  1. 析构函数的调用顺序
    当派生类对象销毁时,先调用派生类的析构函数,再调用基类的析构函数。
    这是为了确保派生类对象的资源(如动态内存)能够先释放,再释放基类资源。

  2. 虚析构函数
    如果你使用了继承,并且派生类需要对基类的析构进行重载,建议将基类的析构函数声明为虚函数。这样可以确保当通过基类指针删除派生类对象时,派生类的析构函数也能正确调用,
    从而避免资源泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
using namespace std;

// 基类
class Animal {
public:
Animal() {
cout << "Animal's constructor called!" << endl;
}

virtual ~Animal() { // 虚析构函数
cout << "Animal's destructor called!" << endl;
}
};

// 派生类
class Dog : public Animal {
public:
Dog() {
cout << "Dog's constructor called!" << endl;
}

~Dog() { // 重载析构函数
cout << "Dog's destructor called!" << endl;
}
};

int main() {
Animal* animal = new Dog(); // 基类指针指向派生类对象
delete animal; // 删除时会调用派生类和基类的析构函数
return 0;
}

/*输出
Animal's constructor called!
Dog's constructor called!
Dog's destructor called!
Animal's destructor called!
*/

继承和派生中的复用

复用 是继承和派生的重要特点之一,它允许派生类直接使用基类已有的代码,而无需重新编写。

复用

派生类会继承基类的属性(成员变量)和方法(成员函数),根据访问权限,派生类可以直接复用基类的成员,或者通过间接访问实现复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class Animal {
public:
void eat() { cout << "I can eat!" << endl; }
void sleep() { cout << "I can sleep!" << endl; }
};

class Dog : public Animal {
public:
void bark() { cout << "I can bark! Woof woof!" << endl; }
};

int main() {
Dog dog;
dog.eat(); // 复用基类方法
dog.sleep(); // 复用基类方法
dog.bark(); // 扩展新功能
return 0;
}

组合继承与复用

虽然继承支持代码复用,但有时会引发一些问题,如脆弱基类问题(基类的改动可能导致派生类问题)。此时可以通过组合代替部分继承,避免不必要的耦合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Engine {
public:
void start() { cout << "Engine started" << endl; }
};

class Car {
private:
Engine engine; // 组合:Car 包含 Engine
public:
void drive() {
engine.start(); // 复用 Engine 的功能
cout << "Car is driving" << endl;
}
};

int main() {
Car car;
car.drive();
return 0;
}

/* 输出
Engine started
Car is driving
*/

继承和派生中的重载

函数重载

函数重载是指在同一个作用域内定义多个同名函数,但这些函数的参数数量或类型不同。它是静态多态的一种形式,因为调用哪个函数是在编译时决定的。
继承与派生关系中,如果基类和派生类中有同名但不同参数的函数,这依然是函数重载,而不是多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;

class Base {
public:
void display() {
cout << "Base: display()" << endl;
}

void display(int x) {
cout << "Base: display(int x) with x = " << x << endl;
}
};

class Derived : public Base {
public:
void display(double y) {
cout << "Derived: display(double y) with y = " << y << endl;
}
};

int main() {
Derived obj;
obj.display(); // 错误!派生类隐藏了基类的同名函数
obj.display(10); // 错误!派生类隐藏了基类的同名函数
obj.display(3.14); // 正确!调用派生类的重载函数
return 0;
}

如果没有特别处理,基类的同名函数会被派生类隐藏,即使参数不同。
为了解决这个问题,可以显式地引入基类的函数:
1
2
3
4
5
6
7
class Derived : public Base {
public:
using Base::display; // 显式引入基类的 display() 方法
void display(double y) {
cout << "Derived: display(double y) with y = " << y << endl;
}
};

结果
1
2
3
4
Derived obj;
obj.display(); // 调用 Base 的 display()
obj.display(10); // 调用 Base 的 display(int)
obj.display(3.14); // 调用 Derived 的 display(double)

函数重写 (动态多态)

函数重写 指派生类重新定义基类中的虚函数

  • 重写要求函数名、参数列表和返回类型完全一致。
  • 基类中的函数必须声明为 virtual,才能被派生类重写。
  • 函数重写是多态的基础,派生类的实现可以通过基类指针或引用调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class Base {
public:
virtual void show() { // 基类中的虚函数
cout << "Base: show()" << endl;
}
};

class Derived : public Base {
public:
void show() override { // 覆盖基类的虚函数,使用override表明是重写函数
cout << "Derived: show()" << endl;
}
};

int main() {
Base* obj = new Derived(); // 基类指针指向派生类对象
obj->show(); // 调用 Derived 的 show()
delete obj;
return 0;
}

如果基类中的虚函数没有具体实现,可以将其声明为纯虚函数,派生类必须重写该函数。

运算符重载 (静态多态的一种形式)

运算符重载是函数重载的一种特殊形式,派生类可以重载基类的运算符。

多继承

多继承 是指一个派生类同时继承多个基类,从而能够复用多个基类的功能。C++ 支持多继承,这是与一些其他编程语言(如 Java)的重要区别之一。

多继承的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base1 {
public:

};

class Base2 {
public:

};

// Derived 从 Base1 和 Base2 继承
class Derived : public Base1, public Base2 {
public:

};

多继承中的冲突

名字冲突

如果基类之间有相同名字的成员函数或变量,派生类中访问这些成员可能会产生二义性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base1 {
public:
void show() { cout << "Base1 show" << endl; }
};

class Base2 {
public:
void show() { cout << "Base2 show" << endl; }
};

class Derived : public Base1, public Base2 {
};

int main() {
Derived obj;
// obj.show(); // 编译错误:二义性
obj.Base1::show(); // 通过基类名解决二义性
obj.Base2::show(); // 指定调用 Base2 的 show
return 0;
}

菱形继承问题

菱形继承指的是派生类从两个基类继承,而这两个基类又继承自同一个基类。这会导致基类的成员在派生类中存在多份副本,可能引发冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
public:
void show() { cout << "A show" << endl; }
};

class B : public A {
};

class C : public A {
};

class D : public B, public C {
};

int main() {
D obj;
// obj.show(); // 编译错误:二义性,A 的成员有两份
obj.B::show(); // 指定调用 B 中的 A
obj.C::show(); // 指定调用 C 中的 A
return 0;
}

为了解决这个问题,C++ 提供了虚继承(virtual inheritance)机制,使得派生类只继承基类的一份成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public:
void show() { cout << "A show" << endl; }
};

class B : virtual public A {
};

class C : virtual public A {
};

class D : public B, public C {
};

int main() {
D obj;
obj.show(); // 正确,调用 A 的 show
return 0;
}

虚基类

虚基类 是 C++ 中用于解决菱形继承问题和确保唯一实例的机制。它通过使一个基类在派生类中只保留一份实例,避免了重复继承引发的冲突和资源浪费。

虚基类通过在继承声明中使用 virtual 关键字来指定。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public:
void show() { cout << "A show" << endl; }
};

class B : virtual public A {
};

class C : virtual public A {
};

class D : public B, public C {
};

int main() {
D obj;
obj.show(); // 正确,调用 A 的 show
return 0;
}

构造函数与虚基类

  • 虚基类的构造函数由最派生类负责调用。
  • 即使多个中间基类继承虚基类,也只会构造一份虚基类实例。
  • 调用顺序:虚基类的构造函数在派生类构造函数之前被调用。

虚基类成员初始化
派生类需要通过自己的构造函数初始化虚基类的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class A {
public:
int value;
A(int v) : value(v) { cout << "A constructor with value = " << value << endl; }
};

class B : virtual public A {
public:
B() : A(0) { cout << "B constructor" << endl; }
};

class C : virtual public A {
public:
C() : A(0) { cout << "C constructor" << endl; }
};

class D : public B, public C {
public:
D() : A(42), B(), C() { cout << "D constructor" << endl; } // 初始化基类成员
};

int main() {
D obj;
cout << "D's A::value = " << obj.value << endl;
return 0;
}

/* 输出
A constructor with value = 42
B constructor
C constructor
D constructor
D's A::value = 42
*/

多继承中的构造和析构

在多继承中,构造函数的调用顺序按照基类在继承列表中的声明顺序执行。对于虚基类,无论声明顺序如何,总是在最派生类(最终派生类)中初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using namespace std;

class A {
public:
A() { cout << "A constructor" << endl; }
};

class B : public A {
public:
B() { cout << "B constructor" << endl; }
};

class C : public A {
public:
C() { cout << "C constructor" << endl; }
};

class D : public B, public C {
public:
D() { cout << "D constructor" << endl; }
};

int main() {
D obj;
return 0;
}

/* 输出
A constructor
B constructor
A constructor
C constructor
D constructor
*/

可以看到,每个基类都调用 A 的构造函数,导致重复初始化。
解决方法:虚基类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class A {
public:
A() { cout << "A constructor" << endl; }
};

class B : virtual public A {
public:
B() { cout << "B constructor" << endl; }
};

class C : virtual public A {
public:
C() { cout << "C constructor" << endl; }
};

class D : public B, public C {
public:
D() { cout << "D constructor" << endl; }
};

int main() {
D obj;
return 0;
}

/* 输出
A constructor
B constructor
C constructor
D constructor
*/

虚基类的构造函数由最派生类负责调用,避免重复初始化。