C++基础:0d2面向对象

[TOC]

构造对象

在C++中,classstruct 都可以用于定义自定义类型,并且它们都可以包含成员变量和成员函数。然而,它们在默认访问权限和继承方式上有一些区别。

1. 默认访问权限

  • class: 默认的成员访问权限是 private。这意味着如果你不显式指定访问权限,类的成员变量和成员函数将是私有的,只能在类的内部访问。
  • struct: 默认的成员访问权限是 public。这意味着如果你不显式指定访问权限,结构体的成员变量和成员函数将是公有的,可以在类的外部访问。
1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
int x; // 默认是 private
public:
void setX(int val) { x = val; }
int getX() const { return x; }
};

struct MyStruct {
int x; // 默认是 public
void setX(int val) { x = val; }
int getX() const { return x; }
};

2. 默认继承方式

  • class: 默认的继承方式是 private 继承。这意味着如果你不显式指定继承方式,派生类将私有继承基类。
  • struct: 默认的继承方式是 public 继承。这意味着如果你不显式指定继承方式,派生类将公有继承基类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BaseClass {
public:
void foo() {}
};

class DerivedClass : BaseClass { // 默认是 private 继承
// DerivedClass 私有继承 BaseClass
};

struct BaseStruct {
void foo() {}
};

struct DerivedStruct : BaseStruct { // 默认是 public 继承
// DerivedStruct 公有继承 BaseStruct
};

3. 使用习惯

  • class: 通常用于表示具有复杂行为和封装需求的对象。类的成员变量通常是私有的,通过公共的成员函数来访问和修改。
  • struct: 通常用于表示简单的数据结构,尤其是当数据成员需要直接访问时。结构体常用于数据聚合,而不涉及复杂的行为。

构造 、析构、与拷贝构造

在C++中,构造函数析构函数拷贝构造函数 是类的特殊成员函数,用于管理对象的生命周期和资源。它们分别负责对象的初始化、清理和拷贝操作。

1. 构造函数(Constructor)

构造函数在创建对象时自动调用,用于初始化对象的成员变量或分配资源。

  • 函数名与类名相同。
  • 没有返回类型(包括 void)。
  • 可以重载(即可以有多个构造函数)。
  • 如果没有显式定义构造函数,编译器会生成一个默认的无参构造函数。
  • 如果定义了任何构造函数(包括拷贝构造函数),编译器不会生成默认的无参构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass {
public:
int x;
int y;

// 默认构造函数
MyClass() : x(0), y(0) {
std::cout << "Default constructor called!" << std::endl;
}

// 带参数的构造函数
MyClass(int a, int b) : x(a), y(b) {
std::cout << "Parameterized constructor called!" << std::endl;
}
};

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

2. 析构函数(Destructor)

析构函数在对象销毁时自动调用,用于释放对象占用的资源(如动态内存、文件句柄等)。

  • 函数名是类名前加 ~
  • 没有返回类型(包括 void)。
  • 没有参数,因此不能重载。
  • 如果没有显式定义析构函数,编译器会生成一个默认的析构函数。
  • 通常用于释放动态分配的内存或关闭文件等清理操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass {
public:
int* ptr;

// 构造函数
MyClass() {
ptr = new int(100); // 动态分配内存
std::cout << "Constructor called!" << std::endl;
}

// 析构函数
~MyClass() {
delete ptr; // 释放内存
std::cout << "Destructor called!" << std::endl;
}
};

int main() {
MyClass obj; // 构造函数调用
return 0; // 对象销毁时,析构函数调用
}

3. 拷贝构造函数(Copy Constructor)

拷贝构造函数用于通过一个已存在的对象初始化一个新对象。通常用于深拷贝(deep copy)操作。

  • 函数名与类名相同。
  • 参数是对同类型对象的常量引用(const T&)。
  • 如果没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数(执行浅拷贝)。
  • 默认的拷贝构造函数可能不适合需要深拷贝的场景(如动态内存管理)。
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* ptr;

// 构造函数
MyClass(int value) {
ptr = new int(value); // 动态分配内存
std::cout << "Constructor called!" << std::endl;
}

// 拷贝构造函数
MyClass(const MyClass& other) {
ptr = new int(*other.ptr); // 深拷贝
std::cout << "Copy constructor called!" << std::endl;
}

// 析构函数
~MyClass() {
delete ptr; // 释放内存
std::cout << "Destructor called!" << std::endl;
}
};

int main() {
MyClass obj1(100); // 调用构造函数
MyClass obj2 = obj1; // 调用拷贝构造函数
return 0;
}

注意事项

  1. 浅拷贝 vs 深拷贝

    • 默认的拷贝构造函数执行浅拷贝(直接复制指针地址),可能导致多个对象共享同一块内存。
    • 如果类中有动态分配的资源(如指针),需要显式定义拷贝构造函数以实现深拷贝。
    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
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    #include <iostream>
    #include <string>

    using namespace std;

    class MyClass {
    private:
    string name;
    int age;
    int *data;
    int size;
    public:

    MyClass(string n, int a,int *d,int size) {
    name = n;
    age = a;
    this->size = size;
    data = new int[size];
    for(int i=0;i<size;i++)
    data[i] = d[i];
    }
    ~MyClass() {
    delete[] data;
    }
    // //浅拷贝不复制内存空间,会造成内存错误
    // MyClass(const MyClass& other) {
    // name = other.name;
    // age = other.age;
    // data = other.data;
    // size = other.size;
    // }
    //深拷贝复制内存空间
    MyClass(const MyClass& other) {
    name = other.name;
    age = other.age;
    size = other.size;
    data = new int[size];
    for(int i=0;i<size;i++)
    data[i] = other.data[i];
    }

    void print() {
    cout << "Name: " << name << endl;
    cout << "Age: " << age << endl;
    for(int i=0;i<10;i++)
    cout << data[i] << " ";
    cout << endl;
    }
    };

    int main() {
    int data[10] = {1,2,3,4,5,6,7,8,9,10};
    MyClass obj1("John", 25,data,sizeof(data)/sizeof(int));
    obj1.print();
    MyClass obj2(obj1);
    obj2.print();
    return 0;
    }

  1. 避免拷贝

    • 如果不希望对象被拷贝,可以将拷贝构造函数声明为 delete
      1
      MyClass(const MyClass&) = delete;
  2. 移动语义

移动语义(Move Semantics)和移动赋值(Move Assignment)是C++11引入的重要特性,旨在优化资源管理,特别是对于那些包含动态分配内存的对象。它们通过允许对象在可能的情况下“窃取”资源而不是进行深拷贝,从而提高了程序的性能。

移动语义的核心思想是通过转移资源的所有权来避免不必要的拷贝操作。当你有一个临时对象(比如函数返回值),你不需要对其进行深拷贝,而是可以直接将资源从临时对象转移到目标对象。这在处理大型数据结构时特别有用,因为它可以显著减少拷贝时间和内存使用。

为了支持移动语义,C++11引入了右值引用(rvalue reference),其语法形式为 T&&。右值引用主要用于标识临时对象,因为这些对象通常只能从右值引用绑定到。基于此,C++11还提供了移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。

  • 移动构造函数:典型的声明形式是 X::X(X&& other);。它用于当一个对象被用作临时对象初始化另一个对象时。移动构造函数会“窃取”参数中的资源,并将其自身设置为一种安全状态。

  • 移动赋值运算符:典型的声明形式是 X& X::operator=(X&& other);。它用于当一个对象被用作临时对象赋值给另一个已经存在的对象时。与移动构造函数类似,它也会执行资源的转移。

    4.移动赋值:

移动赋值是指利用移动赋值运算符实现的一种资源转移方式。与传统的拷贝赋值不同,移动赋值不会复制源对象的数据成员,而是尝试将资源的所有权从源对象转移到目标对象。这样做不仅可以节省时间,还可以避免额外的内存分配。

一个简单的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
public:
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) { // 避免自我赋值
// 释放当前对象拥有的资源
delete[] data;

// “窃取”other的数据成员
data = other.data;

// 将other置于安全状态
other.data = nullptr;
}
return *this;
}

private:
int* data;
};

调用时

1
MyClass obj2 = std::move(obj1);

在这个例子中,移动赋值运算符首先检查是否是自我赋值的情况,然后释放当前对象持有的任何资源,接着“窃取”其他对象的资源,并将其他对象设置为安全状态(例如,将其指针成员设置为 nullptr),以确保在其析构时不释放已经被转移走的资源。

通过合理使用构造函数、析构函数和拷贝构造函数,可以有效管理对象的生命周期和资源,避免内存泄漏和其他资源管理问题。

友元

友元类(Friend Class)友元函数(Friend Function)* 间接实现类似的功能。

友元类(Friend Class)

友元类是指一个类可以访问另一个类的私有(private)和保护(protected)成员。通过将类声明为友元,可以授予该类访问权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyClass {
private:
int secretValue;

public:
MyClass(int v) : secretValue(v) {}

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

class FriendClass {
public:
void displaySecret(const MyClass& obj) {
cout << "Secret Value: " << obj.secretValue << endl; // 访问私有成员
}
};

int main() {
MyClass obj(42);
FriendClass friendObj;
friendObj.displaySecret(obj); // 输出: Secret Value: 42
return 0;
}

友元函数(Friend Function)

友元函数是指一个非成员函数可以访问类的私有(private)和保护(protected)成员。通过将函数声明为友元,可以授予该函数访问权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass {
private:
int secretValue;

public:
MyClass(int v) : secretValue(v) {}

// 声明友元函数
friend void displaySecret(const MyClass& obj);
};

// 定义友元函数
void displaySecret(const MyClass& obj) {
cout << "Secret Value: " << obj.secretValue << endl; // 访问私有成员
}

int main() {
MyClass obj(42);
displaySecret(obj); // 输出: Secret Value: 42
return 0;
}

运算符重载(Operator Overloading)

运算符重载是C++中的一种特性,允许程序员为用户定义的类型(如类或结构体)重新定义运算符的行为。通过运算符重载,可以使自定义类型的对象像内置类型一样使用运算符(如 +-*/ 等),从而提高代码的可读性和简洁性。

基本概念

  1. 运算符重载的本质

    • 运算符重载实际上是一个特殊的函数,函数名由 operator 关键字后接运算符符号组成(如 operator+)。
  2. 重载的限制

    • 不能创建新的运算符(如 ** 不能重载为幂运算)。
    • 不能改变运算符的优先级和结合性。
    • 不能改变运算符的操作数个数(如 + 必须是二元运算符)。
    • 部分运算符不能重载(如 ::.*.sizeof 等)。

    注:在C++中,.* 是一个特殊的运算符,称为成员指针运算符(Pointer-to-Member Operator)。它用于通过对象和成员指针访问类的成员(数据成员或成员函数)。

  3. 可重载的运算符

    • 算术运算符:+-*/% 等。
    • 关系运算符:==!=<><=>= 等。
    • 逻辑运算符:&&||! 等。
    • 赋值运算符:=+=-= 等。
    • 下标运算符:[]
    • 函数调用运算符:()
    • 流运算符:<<>>
    • 自增自减运算符:++--
    • 其他:newdelete, 等。

运算符重载的实现方式

运算符重载可以通过成员函数全局函数实现。

1. 成员函数形式

  • 将运算符重载函数定义为类的成员函数。
  • 左操作数是当前对象(this 指针指向的对象),右操作数通过参数传递。

示例:重载 + 运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Complex {
private:
double real, imag;

public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}

// 成员函数形式重载 +
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}

void print() const {
cout << real << " + " << imag << "i" << endl;
}
};

int main() {
Complex c1(3, 4), c2(1, 2);
Complex c3 = c1 + c2; // 调用 operator+
c3.print(); // 输出: 4 + 6i
return 0;
}

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
class Complex {
private:
double real, imag;

public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}

// 声明为友元函数,以便访问私有成员
friend Complex operator+(const Complex& c1, const Complex& c2);

void print() const {
cout << real << " + " << imag << "i" << endl;
}
};

// 全局函数形式重载 +
Complex operator+(const Complex& c1, const Complex& c2) {
return Complex(c1.real + c2.real, c1.imag + c2.imag);
}

int main() {
Complex c1(3, 4), c2(1, 2);
Complex c3 = c1 + c2; // 调用 operator+
c3.print(); // 输出: 4 + 6i
return 0;
}


特殊运算符的重载

1. 赋值运算符 =

  • 默认情况下,C++会为类提供默认的赋值运算符。
  • 如果类中有动态内存分配,需要重载赋值运算符以避免浅拷贝问题。

示例:

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 MyString {
private:
char* str;

public:
MyString(const char* s = "") {
str = new char[strlen(s) + 1];
strcpy(str, s);
}

// 重载赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) { // 防止自赋值
delete[] str; // 释放原有内存
str = new char[strlen(other.str) + 1];
strcpy(str, other.str);
}
return *this;
}

~MyString() {
delete[] str;
}

void print() const {
cout << str << endl;
}
};

2. 下标运算符 []

  • 用于像数组一样访问类的成员。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
class Array {
private:
int arr[10];

public:
int& operator[](int index) {
if (index < 0 || index >= 10) {
throw out_of_range("Index out of range");
}
return arr[index];
}
};

3. 流运算符 <<>>

  • 用于自定义输入输出行为。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Complex {
private:
double real, imag;

public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}

friend ostream& operator<<(ostream& os, const Complex& c);
friend istream& operator>>(istream& is, Complex& c);
};

ostream& operator<<(ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}

istream& operator>>(istream& is, Complex& c) {
is >> c.real >> c.imag;
return is;
}

内联函数(Inline Function)

内联函数是C++中的一种函数优化机制,通过在编译时将函数体直接插入到调用处,从而减少函数调用的开销(如栈帧的创建和销毁)。内联函数适用于短小且频繁调用的函数。类似于宏函数,只不过调用时机不同,内联函数由编译器编译时实现。

  • 使用 inline 关键字只是向编译器提出内联建议,最终是否内联由编译器决定。现代编译器通常会自动优化,即使没有 inline 关键字,也可能将短小函数内联。

语法

在函数声明或定义前加上 inline 关键字:

1
2
3
inline int add(int a, int b) {
return a + b;
}

使用场景

  1. 短小的函数

    • 如简单的数学运算、getter/setter 函数等。
  2. 频繁调用的函数

    • 如果某个函数在循环中被频繁调用,内联可以显著提升性能。
  3. 替代宏函数

    • 内联函数比宏函数更安全(宏函数没有类型检查,容易出错)。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

// 内联函数
inline int max(int a, int b) {
return (a > b) ? a : b;
}

int main() {
int x = 10, y = 20;
cout << "Max value: " << max(x, y) << endl; // 调用处会直接替换为 (x > y) ? x : y
return 0;
}

注意事项

  1. 避免滥用

    • 内联函数会增加代码体积,滥用可能导致程序体积过大,反而降低性能。
  2. 定义在头文件中

    • 内联函数通常定义在头文件中,因为编译器需要在每个调用处看到函数体。如果定义在源文件中,链接时可能会出错。
  3. 递归函数不能内联

    • 递归函数无法在编译时展开,因此不能内联。
  4. 虚函数不能内联

    • 虚函数的调用是动态绑定的,无法在编译时确定具体调用的函数,因此不能内联。

内联函数 vs 宏函数

特性 内联函数 宏函数
类型检查
调试支持 支持 不支持
作用域 遵循C++作用域规则 无作用域,全局替换
安全性 更安全 容易出错(如副作用)
性能 由编译器优化 直接文本替换

继承

继承(Inheritance) 是 C++ 面向对象编程(OOP)的核心特性之一。它允许一个类(派生类)基于另一个类(基类)来创建,从而复用基类的代码并扩展其功能。继承是实现代码复用、层次化设计和多态性的重要手段。


继承的基本概念

  1. 基类(Base Class)

    • 也称为父类或超类。
    • 是被继承的类,提供通用的属性和行为。
  2. 派生类(Derived Class)

    • 也称为子类。
    • 继承自基类,可以复用基类的成员,并可以添加新的成员或重写基类的成员。
  3. 继承关系

    • 派生类继承了基类的所有成员(除了构造函数、析构函数和私有成员)。
    • 派生类可以访问基类的公有(public)和保护(protected)成员。

继承的语法

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

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

class DerivedClass : access-specifier BaseClass {
// 派生类成员
};
  • access-specifier 是访问修饰符,可以是 publicprotectedprivate
  • 访问修饰符决定了基类成员在派生类中的访问权限。

访问修饰符的作用

  1. public 继承

    • 基类的 public 成员在派生类中仍然是 public
    • 基类的 protected 成员在派生类中仍然是 protected
    • 基类的 private 成员在派生类中不可访问。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Base {
    public:
    int publicVar;
    protected:
    int protectedVar;
    private:
    int privateVar;
    };

    class Derived : public Base {
    // publicVar 是 public
    // protectedVar 是 protected
    // privateVar 不可访问
    };
  2. protected 继承

    • 基类的 publicprotected 成员在派生类中都变为 protected
    • 基类的 private 成员在派生类中不可访问。
    1
    2
    3
    4
    5
    class Derived : protected Base {
    // publicVar 是 protected
    // protectedVar 是 protected
    // privateVar 不可访问
    };
  3. private 继承

    • 基类的 publicprotected 成员在派生类中都变为 private
    • 基类的 private 成员在派生类中不可访问。
    1
    2
    3
    4
    5
    class Derived : private Base {
    // publicVar 是 private
    // protectedVar 是 private
    // privateVar 不可访问
    };

继承的类型

  1. 单继承

    • 一个派生类只继承一个基类。
    • 例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Animal {
      public:
      void eat() {
      cout << "Eating..." << endl;
      }
      };

      class Dog : public Animal {
      public:
      void bark() {
      cout << "Barking..." << endl;
      }
      };
  2. 多重继承

    • 一个派生类继承多个基类。
    • 例如:

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

      class B {
      public:
      void funcB() {
      cout << "Function B" << endl;
      }
      };

      class C : public A, public B {
      public:
      void funcC() {
      cout << "Function C" << endl;
      }
      };
  3. 多级继承

    • 一个派生类继承自另一个派生类。
    • 例如:

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

      class Mammal : public Animal {
      public:
      void breathe() {
      cout << "Breathing..." << endl;
      }
      };

      class Dog : public Mammal {
      public:
      void bark() {
      cout << "Barking..." << endl;
      }
      };
  4. 菱形继承(虚继承)

    • 解决多重继承中基类子对象重复的问题。
    • 例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      class A {
      public:
      int value;
      };

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

      class D : public B, public C {};

构造函数和析构函数的调用顺序

  1. 构造函数调用顺序

    • 基类的构造函数。
    • 派生类的构造函数。
  2. 析构函数调用顺序

    • 派生类的析构函数。
    • 基类的析构函数。

继承的优点

  1. 代码复用:派生类可以复用基类的代码,减少重复代码。
  2. 扩展性:派生类可以扩展基类的功能。
  3. 多态性:通过基类指针或引用调用派生类的函数,实现运行时多态。

继承的缺点

  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
#include <iostream>
using namespace std;

// 基类
class Animal {
public:
void eat() {
cout << "Eating..." << endl;
}
};

// 派生类
class Dog : public Animal {
public:
void bark() {
cout << "Barking..." << endl;
}
};

int main() {
Dog dog;
dog.eat(); // 继承自 Animal
dog.bark(); // Dog 的成员函数
return 0;
}

总结

继承是 C++ 中实现代码复用和层次化设计的重要工具。通过合理使用继承,可以构建清晰、可扩展的类层次结构。然而,过度使用继承(尤其是多重继承)可能会导致代码复杂性和紧耦合问题,因此需要谨慎设计。

虚继承

虚继承(Virtual Inheritance) 是 C++ 中用于解决多重继承中出现的 菱形继承问题(Diamond Problem) 的一种机制。在多重继承中,如果一个类从两个或多个类继承,而这些基类又共同继承自同一个基类,就会导致派生类中包含多个相同的基类子对象,从而引发数据冗余和二义性问题。虚继承可以确保在继承链中共享同一个基类子对象,从而避免这些问题。


菱形继承问题(Diamond Problem)

考虑以下继承关系:

1
2
3
4
5
  A
/ \
B C
\ /
D
  • B 和类 C 都继承自类 A
  • D 同时继承自类 B 和类 C

在这种情况下,类 D 会包含两个 A 的子对象(一个来自 B,一个来自 C),这会导致以下问题:

  1. 数据冗余:类 D 中有两份 A 的成员变量。
  2. 二义性:如果 A 中有成员变量或函数,类 D 无法直接访问它们,因为编译器不知道应该使用 B 中的 A 还是 C 中的 A

虚继承的作用

虚继承通过确保在继承链中共享同一个基类子对象来解决菱形继承问题。具体来说:

  • 当类 B 和类 C 使用虚继承的方式继承类 A 时,类 D 只会包含一个 A 的子对象。
  • 这样,类 D 可以直接访问 A 的成员,而不会产生二义性。

虚继承的语法

在继承时使用 virtual 关键字即可实现虚继承。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
public:
int value;
};

class B : virtual public A {
// B 的成员
};

class C : virtual public A {
// C 的成员
};

class D : public B, public C {
// D 的成员
};
  • BC 都虚继承自 A
  • D 继承 BC 时,D 中只会有一个 A 的子对象。

虚继承的工作原理

虚继承通过引入一个 虚基类指针(Virtual Base Table Pointer) 来实现共享基类子对象。具体来说:

  1. 每个虚继承的类(如 BC)会包含一个指向共享基类(如 A)的指针。
  2. 当派生类(如 D)被实例化时,编译器会确保只有一个 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
using namespace std;

// 基类 A
class A {
public:
int value;
A() : value(0) {
cout << "A constructor called." << endl;
}
~A() {
cout << "A destructor called." << endl;
}
};

// 类 B 虚继承自 A
class B : virtual public A {
public:
B() {
cout << "B constructor called." << endl;
}
~B() {
cout << "B destructor called." << endl;
}
};

// 类 C 虚继承自 A
class C : virtual public A {
public:
C() {
cout << "C constructor called." << endl;
}
~C() {
cout << "C destructor called." << endl;
}
};

// 类 D 继承自 B 和 C
class D : public B, public C {
public:
D() {
cout << "D constructor called." << endl;
}
~D() {
cout << "D destructor called." << endl;
}
};

int main() {
D d;
d.value = 10; // 直接访问 A 的成员,没有二义性
cout << "Value in D: " << d.value << endl;
return 0;
}

输出结果

1
2
3
4
5
6
7
8
9
A constructor called.
B constructor called.
C constructor called.
D constructor called.
Value in D: 10
D destructor called.
C destructor called.
B destructor called.
A destructor called.

关键点

  1. 共享基类子对象:虚继承确保在继承链中只有一个基类子对象被创建。
  2. 解决二义性:派生类可以直接访问基类的成员,而不会产生二义性。
  3. 性能开销:虚继承会引入额外的虚基类指针,可能会增加内存开销和访问时间。

虚继承的应用场景

虚继承通常用于以下场景:

  1. 解决菱形继承问题:当多重继承导致基类子对象重复时。
  2. 共享基类状态:当多个派生类需要共享同一个基类子对象的状态时。

总结

虚继承是 C++ 中解决多重继承问题的强大工具,但需要谨慎使用,因为它会引入额外的复杂性。在设计类继承关系时,尽量避免复杂的多重继承结构,优先使用组合而非继承。

C++ 面向对象精选题

  1. 关于C++中类与结构体的默认访问权限,正确的是:
    A) class默认public,struct默认private
    B) class默认private,struct默认public
    C) class和struct都默认private
    D) class和struct都默认public

  2. 下列哪种情况会调用拷贝构造函数:
    A) 对象通过值传递方式传入函数
    B) 对象通过引用传递方式传入函数
    C) 对象调用成员函数时
    D) 对象使用移动语义转移资源时

  3. 关于友元函数的描述错误的是:
    A) 友元函数可以访问类的私有成员
    B) 友元函数需要在类内部声明
    C) 友元函数会破坏封装性
    D) 友元函数可以是另一个类的成员函数

  4. 重载赋值运算符时,正确的做法是:
    A) 必须返回对象的引用
    B) 应该处理自赋值情况
    C) 参数应为const引用
    D) 以上全部正确

  5. 关于内联函数说法正确的是:
    A) 递归函数适合声明为内联
    B) 虚函数不能是内联函数
    C) 内联函数会减少函数调用开销
    D) 内联函数必须定义在头文件中

  6. 继承时构造函数调用顺序是:
    A) 基类->成员对象->派生类
    B) 成员对象->基类->派生类
    C) 基类->派生类->成员对象
    D) 派生类->基类->成员对象

  7. 解决菱形继承问题的正确方法是:
    A) 使用多重继承
    B) 使用模板元编程
    C) 使用虚继承
    D) 使用友元类

  8. 关于多态的实现机制,正确的是:
    A) 通过函数重载实现
    B) 通过模板特化实现
    C) 通过虚函数表实现
    D) 通过内联函数实现

  9. 类模板的显式特化正确的是:
    A) template<> class MyClass {};
    B) template class MyClass {};
    C) template class MyClass {};
    D) template class MyClass {};

  10. 关于纯虚函数说法错误的是:
    A) 含有纯虚函数的类是抽象类
    B) 纯虚函数可以有函数体
    C) 派生类必须实现所有纯虚函数
    D) 纯虚函数用=0语法声明

选择题答案

  1. B
    类默认访问权限是private,结构体默认是public。这是C++为兼容C结构体而设计的特性。

  2. A 解析:

    1. A) 对象通过值传递方式传入函数
      当一个对象通过值传递方式传入函数时,会调用拷贝构造函数来创建该对象的副本。这是拷贝构造函数的典型使用场景。
    2. B) 对象通过引用传递方式传入函数
      当对象通过引用传递时,不会创建新的对象副本,因此不会调用拷贝构造函数。
    3. C) 对象调用成员函数时
      调用成员函数时,不会涉及对象的拷贝,因此不会调用拷贝构造函数。
    4. D) 对象使用移动语义转移资源时
      使用移动语义时,调用的是移动构造函数(move constructor),而不是拷贝构造函数。
  3. D
    友元函数不能是另一个类的成员函数,只能是普通函数或其他类的友元成员函数。

  4. D
    赋值运算符应返回*this的引用,处理自赋值,参数用const引用避免不必要的拷贝。

  5. C
    内联的核心作用是减少调用开销类似于宏定义。递归函数不适合内联,虚函数可以内联(当静态绑定时),内联函数不强制在头文件。

  6. A
    构造顺序:基类构造函数->成员对象构造函数->派生类构造函数。

  7. C
    虚继承通过共享基类子对象解决菱形继承导致的二义性和数据冗余问题。

  8. C
    动态多态通过虚函数表实现运行时多态。函数重载是静态多态,模板属于参数多态。

  9. A
    显式全特化需要template<>前缀。B是偏特化,D语法错误。

  10. C
    派生类可以不实现纯虚函数(保持抽象类),但只有实现全部纯虚函数才能实例化。

​ B解析

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
#include <iostream>
using namespace std;

// 抽象类
class Base {
public:
// 纯虚函数,用 = 0 声明
virtual void pureVirtualFunction() const = 0;
};

// 纯虚函数的函数体可以在类外定义
void Base::pureVirtualFunction() const {
cout << "This is the body of the pure virtual function in Base class." << endl;
}

// 派生类
class Derived : public Base {
public:
// 派生类必须重写纯虚函数
void pureVirtualFunction() const override {
// 调用基类的纯虚函数实现
Base::pureVirtualFunction();
cout << "This is the overridden function in Derived class." << endl;
}
};

int main() {
// Base base; // 错误:不能实例化抽象类
Derived derived;
derived.pureVirtualFunction(); // 调用派生类的重写函数
return 0;
}

C解析:

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
45
46
#include <iostream>
using namespace std;

// 抽象基类
class Base {
public:
// 纯虚函数 1
virtual void pureVirtualFunction1() const = 0;

// 纯虚函数 2
virtual void pureVirtualFunction2() const = 0;
};

// 派生类 1:实现了所有纯虚函数
class Derived1 : public Base {
public:
void pureVirtualFunction1() const override {
cout << "Derived1: Implementation of pureVirtualFunction1" << endl;
}

void pureVirtualFunction2() const override {
cout << "Derived1: Implementation of pureVirtualFunction2" << endl;
}
};

// 派生类 2:只实现了一个纯虚函数
class Derived2 : public Base {
public:
void pureVirtualFunction1() const override {
cout << "Derived2: Implementation of pureVirtualFunction1" << endl;
}

// 未实现 pureVirtualFunction2,因此 Derived2 仍然是抽象类
};

int main() {
// 实例化 Derived1,它是具体类
Derived1 d1;
d1.pureVirtualFunction1();
d1.pureVirtualFunction2();

// 错误:不能实例化抽象类 Derived2
// Derived2 d2; // 这行代码会导致编译错误

return 0;
}

判断题

  1. 结构体的默认继承方式是public,而类的默认继承方式是private
  2. 析构函数可以声明为虚函数,但构造函数不能声明为虚函数
  3. 友元类的友元关系是单向的,且不具有传递性
  4. 重载流运算符<<时,必须重载为类的友元全局函数 (存疑选做)
  5. 内联函数相比宏函数具有类型安全检查的优势
  6. 函数模板的实例化发生在编译阶段
  7. 虚函数的默认参数在运行时动态绑定
  8. 类模板的静态成员变量在不同特化版本间共享
  9. 移动构造函数应该将源对象的资源指针置为nullptr
  10. 虚继承通过增加虚基类指针来解决菱形继承问题

答案与解析


  1. C++规定class默认继承方式为private,struct为public,保持与C兼容性


  2. 构造函数需要明确类型信息不能虚化,析构函数声明为虚函数确保正确调用派生类析构


  3. A是B的友元类 ≠ B是A的友元类,友元关系不可传递(A→B→C ≠ A→C)


  4. 流运算符左操作数是ostream对象,必须定义为全局函数,通常需要访问私有成员故声明为友元


  5. 宏函数只是文本替换,内联函数会进行类型检查,且支持调试(可用debug模式禁用内联)


  6. 模板在编译时根据具体类型生成实际代码,属于编译期多态机制

  7. ×
    虚函数的默认参数在编译时根据静态类型确定,与动态绑定的函数体行为分离,可能引发二义性

  8. ×
    每个特化版本的类模板都有自己的静态成员实例,如MyClass::count和MyClass::count是独立变量


  9. 移动构造后源对象应处于可析构状态,置空指针避免资源被双重释放


  10. 虚继承通过虚基类表指针(vbptr)让派生类共享同一基类实例,消除菱形继承中的重复基类


填空题

  1. 在C++中,struct的默认访问权限是__,而class的默认继承方式是__
  2. 当派生类对象被构造时,会首先调用__的构造函数,最后调用__的构造函数
  3. 友元类的特性是:友元关系是__向的,且不具有__
  4. 重载流运算符<<时,必须将其声明为__函数形式,通常需要定义为类的__(存疑选做)
  5. 内联函数相比宏函数的优势包括:支持__、具有__检查
  6. 模板的实例化发生在__阶段,属于__期多态机制
  7. 虚函数的默认参数值在__时确定,遵循__绑定规则
  8. 类模板的不同特化版本的静态成员变量是__的,例如MyClass::count与MyClass::count是__变量
  9. 移动构造函数执行后,应将源对象的资源指针置为__,这是为了__
  10. 虚继承通过引入__指针,使得派生类共享同一个基类实例,解决__问题

答案与解析

  1. public, private
    C++规定struct成员默认public访问权限,class继承默认private方式,体现封装差异

  2. 基类, 派生类
    构造顺序:基类成员→基类构造→派生类成员→派生类构造;析构顺序相反

  3. 单, 传递
    A是B的友元 ≠ B是A的友元,友元关系不能继承(A→B→C ≠ A→C)

  4. 全局, 友元
    运算符左操作数为ostream对象,需访问类私有成员,必须声明为友元全局函数

  5. 调试, 类型安全
    宏函数在预处理阶段展开,无类型检查;内联函数在编译期处理,可配合调试器使用

  6. 编译, 编译
    模板根据具体类型在编译期生成特化代码,与运行期多态(虚函数)形成对比

  7. 编译, 静态
    默认参数值根据调用时的静态类型确定,与虚函数动态绑定分离,可能产生二义性

  8. 独立, 不同
    每个模板特化版本都是独立类,静态成员在不同特化类之间不共享

  9. nullptr, 避免重复释放
    移动构造转移资源所有权后,源对象应处于有效但空的状态,防止析构时释放已转移资源

  10. 虚基类表(vbptr), 菱形继承
    虚继承通过vbptr找到共享的基类子对象,消除菱形继承中的多个基类副本问题


代码补全题

  1. 类继承与访问控制
1
2
3
4
5
6
7
8
9
10
11
12
class Base {
int x;
protected:
void foo() {}
};
class Derived : /* 填空1 */ Base { // 默认继承方式
public:
void bar() {
foo(); // 填空2:访问权限是否合法?
/* 填空3:如何访问Base的x成员?*/
}
};
  1. 移动构造函数实现
1
2
3
4
5
6
7
8
9
10
class ResourceHolder {
int* data;
public:
ResourceHolder(ResourceHolder&& other) noexcept
: data(/* 填空1 */) {
other.data = /* 填空2 */;
}
~ResourceHolder() { delete data; }
};
//main函数验证
  1. 友元函数重载运算符 (存疑选做)
1
2
3
4
5
6
7
8
9
10
11
12
class Matrix {
int elems[4];
friend Matrix operator+(const Matrix& a, const Matrix& b) {
Matrix res;
for (int i=0; i<4; ++i)
res.elems[i] = /* 填空1 */;
return res;
}
friend std::ostream& /* 填空2 */ (std::ostream& os, const Matrix& m) {
return os << m.elems[0];
}
};
  1. 虚函数与多态
1
2
3
4
5
6
7
8
9
10
11
class Shape {
public:
virtual /* 填空1 */ draw() = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
public:
void draw() /* 填空2 */ override {
cout << "Drawing circle\n";
}
};
  1. 模板特化
1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
class Wrapper {
T value;
public:
void print() { cout << value; }
};

template<>
class Wrapper</* 填空1 */> {
const char* value;
public:
void print() { cout << "Specialized: " << value; }
};
  1. 虚继承初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
class A { int x; };
class B : virtual public A {
public:
B(int a) : A(/* 填空1 */) {}
};
class C : virtual public A {
public:
C(int a) : A(/* 填空2 */) {}
};
class D : public B, public C {
public:
D(int a) : B(a), C(a), /* 填空3 */ {}
};
  1. 内联成员函数
1
2
3
4
5
6
class Validator {
public:
/* 填空1 */ bool check(int value) /* 填空2 */ {
return value > 0;
}
};
  1. 拷贝控制成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class String {
char* buffer;
public:
String(const char* s) {
buffer = new char[strlen(s)+1];
strcpy(buffer, s);
}
~String() { delete[] buffer; }
String(const String& other)
: buffer(/* 填空1 */) {}
String& operator=(const String& rhs) {
if(this != &rhs) {
delete[] buffer;
buffer = new char[strlen(rhs.buffer)+1];
/* 填空2 */
}
return *this;
}
};
  1. 类型转换运算符
1
2
3
4
5
6
7
class SmartBool {
bool value;
public:
explicit operator /* 填空1 */() const {
return value;
}
};
  1. 模板可变参数
1
2
3
4
template<typename... Args>
auto sum(Args... args) {
return (/* 填空1 */ + ...);
}

答案与解析

  1. 填空1:class(默认private继承)
    填空2:合法(protected成员在派生类中可访问)
    填空3:无法直接访问(Base::x是private成员)
    解析:class继承默认private,struct继承默认public。派生类只能访问基类protected/public成员

  2. 填空1:other.data
    填空2:nullptr
    解析:移动构造转移资源所有权,需置空源对象指针防止重复释放,noexcept保证异常安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ResourceHolder {
int* data;
public:
ResourceHolder(int num): data(new int(num)) {}
ResourceHolder(ResourceHolder&& other)
: data(other.data/* 填空1 */) {
other.data = nullptr/* 填空2 */;
}
~ResourceHolder() { delete data; }
int* get() const { return data; }
};

int main() {
ResourceHolder r1(10);
ResourceHolder r2 = std::move(r1); // 移动构造函数
cout << *r2.get() << endl;
cout << *r1.get() << endl; // 输出未定义行为
return 0;
}
  1. 填空1:a.elems[i] + b.elems[i]
    填空2:operator<<
    解析:友元函数可访问私有成员,流运算符必须返回ostream引用以支持链式调用

  2. 填空1:void
    填空2:virtual(或省略)
    解析:纯虚函数用=0标记,override确保正确重写虚函数

  3. 填空1:const char*
    解析:模板特化需匹配原始模板参数类型,const char*特化版本处理字符串

  4. 填空1:a
    填空2:a
    填空3:A(a)
    解析:虚继承中最终派生类负责初始化虚基类,中间类初始化参数被忽略

  5. 填空1:inline
    填空2:const
    解析:类内定义成员函数默认inline,const成员函数保证不修改对象状态

  6. 填空1:new char[strlen(other.buffer)+1]
    填空2:strcpy(buffer, rhs.buffer);
    解析:深拷贝需分配新内存,赋值运算符需处理自赋值和内存释放

  7. 填空1:bool
    解析:类型转换运算符声明格式,explicit防止隐式转换

  8. 填空1:args
    解析:折叠表达式实现参数包求和,(args + …)展开为arg1 + arg2 + …


程序改错题

  1. 构造函数与异常安全
1
2
3
4
5
6
7
8
class DatabaseConn {
Connection* conn;
public:
DatabaseConn(const string& url) {
conn = new Connection(url); // 可能抛出异常
}
~DatabaseConn() { delete conn; }
};
  1. 虚函数重写问题 X

  2. 移动语义实现

1
2
3
4
5
6
class Buffer {
char* data;
public:
Buffer(Buffer&& other) : data(other.data) {}
~Buffer() { delete[] data; }
};
  1. 模板特化错误
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class Processor {
public:
void process(T val) { /*...*/ }
};

template<>
class Processor<int*> {
public:
void process(int val) { /*...*/ }
};
  1. 友元函数作用域 X

  2. 菱形继承问题

1
2
3
4
5
6
7
8
9
class Base { int x; };
class A : public Base {};
class B : public Base {};
class C : public A, public B {};

void test() {
C c;
c.x = 10; // 编译错误
}
  1. 类型转换运算符 X

  2. 异常规范与析构函数 X

  3. 模板偏特化错误 X

  4. lambda捕获问题 X


答案与解析

  1. 错误:构造函数异常导致资源泄漏
    修正:使用RAII管理资源,添加try-catch或使用智能指针
1
2
DatabaseConn(const string& url) try : conn(new Connection(url)) {} 
catch(...) { delete conn; throw; }
  1. 错误:签名不匹配导致隐藏而非重写
    修正:添加const和override
1
void speak() const override { cout << "Woof"; }
  1. 错误:移动后未置空源指针
    修正:添加noexcept并置空
1
Buffer(Buffer&& other) noexcept : data(other.data) { other.data = nullptr; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Buffer {
char* data;
public:
Buffer(char* p) {
data = new char[strlen(p)+1];
strcpy(data, p);
}
Buffer(Buffer&& other){
data = new char[strlen(other.data)+1];
strcpy(data, other.data);
other.data = nullptr;
}
~Buffer() { delete[] data; }
void print() { cout << data<<endl; }
};

int main() {
char temp[14] = "Hello, world!";
Buffer b1(temp);
Buffer b2 = std::move(b1); // 移动构造函数
b2.print();
b1.print();
return 0;
}
  1. 错误:特化版本参数不匹配
    修正:保持参数类型一致
1
void process(int* val) { /*...*/ }
  1. 错误:友元函数声明与定义不匹配
    修正:添加参数到友元声明
1
friend void printMatrix(Matrix& m);
  1. 错误:菱形继承导致二义性
    修正:使用虚继承
1
2
class A : virtual public Base {};
class B : virtual public Base {};
  1. 错误:explicit阻止隐式转换
    修正:显式转换
1
int n = static_cast<int>(r);
  1. 错误:析构函数不应抛出异常
    修正:移除异常规范
1
~FileHandler() noexcept { /*...*/ }
  1. 错误:部分特化语法错误
    修正:正确偏特化语法
1
2
template<typename T>
class Pair<T, int> { /*...*/ }; // 合法偏特化
  1. 错误:值捕获导致多任务共享count
    修正:按引用捕获
1
tasks.emplace_back([&count]() { ... }); // 注意生命周期

问答题

  1. class和struct在默认访问权限和继承方式上有何本质区别?这种设计体现了什么编程思想?
  2. 派生类对象构造时,基类构造函数与成员变量的初始化顺序如何确定?为什么需要这样的顺序?
  3. 友元关系是否具有传递性和对称性?请通过示例说明友元类的作用范围限制
  4. 为什么流运算符<<通常需要重载为全局友元函数?成员函数形式的重载会带来什么限制?
  5. 内联函数相比宏函数有哪些类型安全优势?调试时如何处理内联函数?
  6. 模板实例化与虚函数多态在实现机制上有何本质区别?各自适用于什么场景?
  7. 虚函数的默认参数值为什么建议在基类中统一声明?动态绑定机制如何处理默认参数?
  8. 类模板的不同特化版本的静态成员变量为何相互独立?这种设计带来了什么特性?
  9. 移动构造函数为什么要将源对象指针置空?如果不这样做会导致什么严重后果?
  10. 虚继承如何通过虚基类表解决菱形继承问题?这种方案会带来什么额外开销?

答案与解析

  1. class默认private访问权限和private继承,struct默认public访问权限和public继承
    体现封装性设计:class强调数据隐藏,适合面向对象;struct保持C兼容性,适合数据聚合。继承方式差异确保class的派生类不会意外暴露基类接口。

  2. 基类成员→基类构造→派生类成员→派生类构造
    该顺序保证:①基类完整性优先于派生类 ②成员初始化顺序与声明一致。若顺序颠倒可能导致访问未初始化的基类成员。

  3. 不具有传递性和对称性

    1
    2
    class A { friend class B; };
    class B { friend class C; }; // C不能访问A

    B是A的友元,但A不是B的友元。友元关系仅对直接授予的类有效。

  4. 左操作数为流对象而非类对象
    成员函数形式要求左操作数是类实例(如obj << cout),与自然用法相反。友元全局函数保持cout << obj的直观语法,同时访问私有成员。

  5. 类型检查与调试符号
    宏函数无类型安全检查,内联函数参与编译过程。调试时可强制取消内联(g++ -fno-inline),而宏在预处理后不可见。

  6. 编译期多态 vs 运行期多态
    模板通过代码生成实现静态多态,虚函数通过虚表动态派发。模板适合类型无关算法,虚函数适合运行时类型变化。

  7. 默认参数静态绑定
    默认参数在编译期根据对象静态类型确定,虚函数调用在运行期动态绑定。若派生类修改默认参数会导致与基类行为不一致。

  8. 每个特化都是独立类
    MyClass与MyClass是完全不同的类型,其静态成员如同不同类的静态变量。这保证了类型特化间的完全隔离。

  9. 避免双重释放
    移动后源对象进入有效但不可用状态。若未置空,源对象析构时会释放已转移的资源,导致新对象的资源被意外回收。

  10. 虚基类表指针共享基类实例
    每个派生类包含vbptr指向虚基类表,通过偏移量访问共享基类。开销包括:指针存储空间和间接访问成本,但解决了数据冗余和二义性。