本文是基于《C++新经典》,《Effective C++》, 《C++ primer》以及收集的相关资料总结而来.

成员函数的定义

类的声明和类的实现要分别定义于.h和.cpp文件当中. 声明可以被其它实现多次包含.

重要关注点:

在类定义内部实现的所有成员函数,包括构造函数、普通成员函数和静态成员函数,通常都会被编译器隐式地视为 inline.

函数或变量的定义不仅声明了它,还分配了存储空间,并给出了初始化的值或实现的代码。如果一个函数或变量被定义多次,那么就会分配多次存储空间,这是不允许的。这就是为什么函数或变量只能被定义一次,但可以被声明多次的原因。

1
2
3
4
5
6
7
#include <iostream>
class MyClass {
public:
    void myFunction() {
        std::cout << "Hello, World!" << std::endl;
    }
};

类的使用限制

类的私有成员变量和私有成员函数都只能在类的成员函数内调用.

构造函数

1
2
3
4
5
6
class MyClass {
public:
MyClass(int value) : value_(value) {} // 构造函数在类定义中实现
private:
int value_;
};

构造函数无返回值,以往书写无返回值函数时总在函数返回类型位置书写void,如voidfunc(…),而构造函数是确确实实在函数头什么也不写,这也是构造函数的特殊之处.

正常情况下,构造函数应该被声明为public,因为创建一个对象时系统要调用构造函数,这说明构造函数是一个public函数,能够被外界调用,因为class(类)默认的成员是private(私有)成员,所以必须说明构造函数是一个public函数,否则就无法直接创建该类的对象了(创建对象代码编译时报错)。

单例模式除外.

构造函数是内联的,因为它的定义是在类定义中完成的。然而,如果构造函数的定义是在类定义外部完成的,那么这个构造函数默认是不内联的。因此在.h文件声明+定义构造函数不会直接报错

派生类和基类构造函数调用顺序
1. 基类构造函数:当创建一个派生类对象时,首先调用的是基类的构造函数。这是因为派生类对象包含基类的子对象,必须先初始化基类部分。
2. 派生类构造函数:在基类构造函数完成后,派生类的构造函数才会被调用。

构造顺序:基类构造函数 -> 派生类构造函数
析构顺序:派生类析构函数 -> 基类析构函数

花括号和圆括号的区别

在C++中,构造对象时可以使用圆括号或花括号来传递参数,这两种方式有一些细微的区别,尤其是在C++11及其后的标准中引入了统一的初始化语法(也称为列表初始化或花括号初始化)。以下是它们之间的主要区别:

1. 圆括号初始化

圆括号初始化是传统的C++初始化方式,用于调用构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
MyClass(int value) {
// 构造函数
std::cout << "Constructor called with value: " << value << std::endl;
}
};

int main() {
MyClass obj(42); // 使用圆括号初始化
return 0;
}
  • 优点:直观且与函数调用语法一致。
  • 限制:不能用于初始化内置数组。

2. 花括号初始化(列表初始化)

花括号初始化是C++11引入的特性,提供了一种更统一和安全的初始化方式。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
MyClass(int value) {
// 构造函数
std::cout << "Constructor called with value: " << value << std::endl;
}
};

int main() {
MyClass obj{42}; // 使用花括号初始化
return 0;
}
  • 优点

    • 防止窄化转换:花括号初始化会阻止可能导致数据丢失的隐式窄化转换。例如,从doubleint的转换在花括号初始化中会导致编译错误。
    • 统一初始化语法:可以用于初始化数组、结构体、类对象等。
    • 支持初始化列表:可以直接初始化包含多个元素的容器。
  • 限制:在某些情况下,花括号初始化可能会导致歧义,特别是在构造函数重载时。

3. 区别和注意事项

  • 窄化转换:花括号初始化会阻止窄化转换,而圆括号初始化不会。例如:
1
2
int x = 3.14;  // 允许,x = 3
int y{3.14}; // 错误,窄化转换被禁止
  • 优先级和歧义:在某些情况下,花括号初始化可能会导致歧义,特别是在构造函数重载时。编译器可能无法确定应该调用哪个构造函数。

  • 初始化列表:花括号初始化可以用于初始化列表,而圆括号不能。例如:

1
2
std::vector<int> vec1(10, 1);  // 创建一个包含10个元素的vector,每个元素为1
std::vector<int> vec2{10, 1}; // 创建一个包含两个元素的vector,元素为10和1
  • 圆括号初始化:传统的构造函数调用方式,适用于大多数情况。
  • 花括号初始化:C++11引入的统一初始化方式,提供了更安全的初始化(防止窄化转换)和更广泛的适用性(如初始化列表)。

构造函数初始化列表

构造函数初始化列表是一种高效且清晰的成员变量初始化方式,特别适用于常量成员、引用成员和需要复杂初始化的成员。使用初始化列表可以避免不必要的默认构造和赋值操作,提高代码的性能和可读性。

  • 初始化列表:在构造函数的参数列表之后,使用冒号:引出初始化列表。初始化列表中包含成员变量的初始化表达式。
  • 成员变量的初始化顺序:成员变量的初始化顺序与它们在类中声明的顺序一致,而不是在初始化列表中出现的顺序。因此,建议按照成员变量声明的顺序在初始化列表中进行初始化,以避免潜在的错误。
  • 效率:使用初始化列表可以避免不必要的默认构造和赋值操作.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <string>

class Address {
public:
Address(const std::string& city, const std::string& street) : city_(city), street_(street) {}

void display() const {
std::cout << "City: " << city_ << ", Street: " << street_ << std::endl;
}

private:
std::string city_;
std::string street_;
};
int main() {
Address addr("New York", "5th Avenue");
return 0;
}

成员函数返回自身对象的引用

成员函数可以返回自身对象的引用,通常是通过使用 this 指针来实现的。这种做法常用于实现方法链(method chaining)或流式接口(fluent interface),使得可以连续调用多个成员函数。

const 成员函数

const成员函数代表函数不能修改成员变量,成员函数可以返回自身对象的引用,通常是通过使用 this 指针来实现的。这种做法常用于实现方法链(method chaining)或流式接口(fluent interface),使得可以连续调用多个成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

#include <iostream>
class Example {
private:
    int value;
public:
    Example(int v) : value(v) {}
    // const 成员函数
    int getValue() const {
        return value;
    }
    // 非 const 成员函数
    void setValue(int v) {
        value = v;
    }
};

mutable成员变量

mutable 关键字用于修饰类中的成员变量,使得即使在 const 成员函数中也可以修改这些变量。通常情况下,const 成员函数不能修改类的任何非 const 成员变量,但如果某个成员变量被声明为 mutable,则可以在 const 成员函数中修改它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

class Example {
private:
    mutable int accessCount; // 可在 const 成员函数中修改
    int value;
public:
    Example(int v) : value(v), accessCount(0) {}
    // const 成员函数
    int getValue() const {
        ++accessCount; // 修改 mutable 成员变量
        return value;
    }
    // 获取访问计数
    int getAccessCount() const {
        return accessCount;
    }
};

静态成员函数和变量

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

class Example {
public:
static int count; // 声明静态成员变量
};

// 必须在类外部定义静态成员变量
int Example::count = 0;

int main() {
Example ex1;
Example ex2;

Example::count += 2; // 直接访问静态成员变量

std::cout << "Count: " << Example::count << std::endl; // 输出: Count: 2

return 0;
}
  • 静态成员变量的外部定义:确保只有一份静态成员变量的实例,并且为其分配内存。
  • 避免链接错误:通过在类外部定义,避免了在多个源文件中重复定义同一静态成员变量的问题。

为什静态成员变量需要在类外部定义

  1. 内存分配

静态成员变量的存储是与类本身相关的,而不是与类的任何实例相关。静态成员变量在程序的整个生命周期内存在,并且只有一份拷贝。这意味着它们的内存分配需要在程序启动时进行,而不是在每个对象创建时进行。

  1. 类的定义与变量的定义

在类的定义中,只是声明了静态成员变量,并没有为它分配内存。类的定义只是一个模板,告诉编译器这个类的结构和成员是什么。为了让编译器为静态成员变量分配内存,必须在类外部进行定义。

  1. 避免重复定义

如果静态成员变量在类内部也定义(即在类定义中同时声明和定义),那么在每个包含该类的源文件中都会有一个静态成员变量的定义,这将导致链接错误,因为链接器会发现多个同名的定义。

类内初始化

const 成员变量的初始化

const 成员变量是指在类中声明为 const 的成员变量。这些变量在对象被创建时必须被初始化,并且一旦初始化后,它们的值就不能被修改。因此,const 成员变量的初始化通常需要在构造函数的初始化列表中进行,不能在构造函数当中进行重新赋值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

#include <iostream>

class Example {
private:
    const int id; // const 成员变量
    const std::string name; // const 成员变量
public:
    // 构造函数,使用初始化列表初始化 const 成员变量
    Example(int idValue, const std::string& nameValue)
        : id(idValue), name(nameValue) {}
    // 显示信息
    void display() const {
        std::cout << "ID: " << id << ", Name: " << name << std::endl;
    }
};

默认构造函数

编译器会自动生成一个默认构造函数(default constructor),当类中没有定义任何构造函数时。默认构造函数是一个不带参数的构造函数,用于创建类的实例并初始化其成员变量。

1
2
3
4
5
class MyClass {
public:
// 没有定义构造函数
};
MyClass obj; // 使用自动生成的默认构造函数

显示声明对应的构造函数.

1
2
3
4
class MyClass {
public:
    MyClass() = default// 显式声明默认构造函数
};

一旦程序员书写了自己的构造函数,那么在创建对象的时候,必须提供与书写的构造函数形参相符合的实参,才能成功创建对象。

1
2
3
4
5
class MyClass {
public:
MyClass(int x) {} // 用户定义的构造函数,没有默认构造函数
};
// MyClass obj; // 错误:没有默认构造函数

删除默认构造函数

可以通过多种方式删除构造函数,特别是在需要防止某些类型的对象被创建时。删除构造函数通常用于以下几种情况:
1. 防止对象实例化:如果你希望某个类不能被实例化,可以将其构造函数声明为 delete
2. 控制对象的创建:在某些情况下,你可能希望仅允许某些特定的构造方式,而禁止其他构造方式,单例模式

1
2
3
4
5
6
7
8
9
class MyClass {
public:
    MyClass() = delete;                  // 删除默认构造函数
    MyClass(const MyClass&) = delete;    // 删除拷贝构造函数
    MyClass(MyClass&&) = delete;         // 删除移动构造函数
    MyClass(int value) : value(value) {}
private:
    int value;
};

拷贝构造函数

如果一个类的构造函数的第一个参数是所属的类类型引用,若有额外的参数,那么这些额外的参数都有默认值。该构造函数的默认参数必须放在函数声明中,除非该构造函数没有函数声明,那么这个构造函数就叫拷贝构造函数。

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
#include <iostream>
#include <cstring>
class MyString {
private:
    char* str; // 动态分配的字符串
public:
    // 构造函数
    MyString(const char* s) {
        str = new char[strlen(s) + 1]; // 分配内存
        strcpy(str, s); // 复制字符串
    }
    // 拷贝构造函数
    MyString(const MyString& other) {
        str = new char[strlen(other.str) + 1]; // 分配内存
        strcpy(str, other.str); // 复制字符串
        std::cout << "Copy constructor called!" << std::endl;
    }
    // 析构函数
    ~MyString() {
        delete[] str; // 释放内存
    }
    // 打印字符串
    void display() const {
        std::cout << str << std::endl;
    }
};
int main() {
    MyString str1("Hello, World!");
    MyString str2 = str1; // 调用拷贝构造函数
    str1.display(); // 输出: Hello, World!
    str2.display(); // 输出: Hello, World!
    return 0;
}

参数:拷贝构造函数接受一个同类型的对象的引用作为参数,通常是常量引用(const),以避免不必要的复制。
调用时机
  - 当一个对象被初始化为另一个对象的副本时。
  - 当一个对象作为函数参数传递(按值传递)时。
  - 当一个对象作为函数的返回值时。

如果没有定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数。这个编译器生成的拷贝构造函数会逐个复制对象的所有非静态成员变量(即进行浅拷贝)。

禁止拷贝:如果不希望对象被拷贝,可以将拷贝构造函数和拷贝赋值运算符声明为 delete 或私有

用算符重载

运算符重载通过定义一个特殊的成员函数或友元函数来实现。重载的运算符可以是成员函数或非成员函数,具体取决于运算符的类型和使用场景。

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
#include <iostream>
class Complex {
private:
    double real; // 实部
    double imag; // 虚部
public:
    // 构造函数
    Complex(double r = 0double i = 0) : real(r), imag(i) {}
    // 重载加法运算符
    Complex operator+(const Complex& other) {
        return Complex(real + other.real, imag + other.imag);
    }
    // 重载输出流运算符
    friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
        os << c.real << " + " << c.imag << "i";
        return os;
    }
};
int main() {
    Complex c1(3.04.0);
    Complex c2(1.02.0);
    Complex c3 = c1 + c2; // 调用重载的加法运算符
    std::cout << "c1: " << c1 << std::endl; // 调用重载的输出流运算符
    std::cout << "c2: " << c2 << std::endl;
    std::cout << "c3: " << c3 << std::endl; // 输出: c3: 4 + 6i
    return 0;
}

拷贝赋值运算符重载

用于将一个对象的值赋给另一个已经存在的对象。它的主要作用是在对象之间进行赋值操作时,确保正确地复制对象的状态,尤其是在涉及动态内存分配或其他资源管理时。

赋值运算符既然是一个函数,就有返回类型和参数列表,这里的参数就表示运算对象,如上面myTime5就是运算对象(因为是myTime5要把值赋给myTime6,所以myTime5就是运算对象)

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 MyString {
private:
    char* str; // 动态分配的字符串
public:
    // 构造函数
    MyString(const char* s) {
        str = new char[strlen(s) + 1]; // 分配内存
        strcpy(str, s); // 复制字符串
    }
    // 拷贝构造函数
    MyString(const MyString& other) {
        str = new char[strlen(other.str) + 1]; // 分配内存
        strcpy(str, other.str); // 复制字符串
        std::cout << "Copy constructor called!" << std::endl;
    }
    // 拷贝赋值运算符
    MyString& operator=(const MyString& other) {
        std::cout << "Copy assignment operator called!" << std::endl;
        if (this != &other) { // 自我赋值检查
            delete[] str; // 释放旧内存
            str = new char[strlen(other.str) + 1]; // 分配新内存
            strcpy(str, other.str); // 复制字符串
        }
        return *this// 返回当前对象的引用
    }
    // 析构函数
    ~MyString() {
        delete[] str; // 释放内存
    }
    // 打印字符串
    void display() const {
        std::cout << str << std::endl;
    }
};

运算符的最终结果通常是运算符重载函数返回的结果。运算符重载允许你定义自定义类型(类)如何响应特定的运算符(如 +-*=[]<< 等),并且这些运算符的行为由你实现的函数决定。

赋值拷贝函数返回引用的原因之一支持链式赋值,另外避免效率问题.
链式赋值是指在一个表达式中连续进行多个赋值操作。例如:

1
a = b = c;

在这个例子中,首先执行 b = c,然后将 b 的结果赋值给 a。为了实现这一点,拷贝赋值运算符需要返回一个引用,以便可以将结果传递给下一个赋值操作。

析构函数

析构函数用于在对象生命周期结束时执行清理操作。它的主要作用是释放对象所占用的资源,例如动态分配的内存、打开的文件句柄、网络连接等。

名称:析构函数的名称与类名相同,但前面有一个波浪号(~)。例如,对于类 MyClass,析构函数的名称为 ~MyClass
无参数:析构函数不接受参数,也不返回值。
自动调用:当对象的生命周期结束时(例如,超出作用域或被删除时),析构函数会自动被调用。
每个类只能有一个析构函数,且不能被重载。

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>
#include <cstring>
class MyString {
private:
    char* str; // 动态分配的字符串
public:
    // 构造函数
    MyString(const char* s) {
        str = new char[strlen(s) + 1]; // 分配内存
        strcpy(str, s); // 复制字符串
    }
    // 析构函数
    ~MyString() {
        std::cout << "Destructor called for: " << str << std::endl;
        delete[] str; // 释放内存
    }
    // 打印字符串
    void display() const {
        std::cout << str << std::endl;
    }
};
int main() {
    {
        MyString myStr("Hello, World!");
        myStr.display(); // 输出: Hello, World!
    } // myStr 超出作用域,析构函数被调用
    return 0;
}

虚析构函数:如果一个类有虚函数(例如作为基类),它的析构函数应该是虚拟的,以确保派生类的析构函数被正确调用。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor called" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor called" << std::endl;
}
};

避免多次释放:析构函数中不应释放已经释放的资源。使用智能指针(如 std::unique_ptr 或 std::shared_ptr)可以自动管理资源,减少手动管理的复杂性。
顺序:析构函数的调用顺序是从派生类到基类,确保派生类的资源在基类资源之前被释放。
使用 delete[]:始终确保使用 delete[] 来释放通过 new[] 分配的数组,使用 delete 会导致内存泄漏和未定义行为。

继承机制

基类:被继承的类,提供属性和方法。
派生类:从基类继承的类,可以扩展或修改基类的功能。
访问修饰符:控制基类成员在派生类中的可见性,主要有三种:
  - public:派生类可以访问基类的公共成员。
  - protected:派生类可以访问基类的公共和受保护成员,但外部代码不能访问。
  - private:派生类不能直接访问基类的私有成员。

04c9c4a67be0242391a9c51ea894eee3.png

虚继承

虚继承用于解决多重继承中的“菱形继承”问题,确保基类只被实例化一次。通过在基类前加上 virtual 关键字实现虚继承。

假设有一个基类 A,两个派生类 B 和 C 分别从 A 继承,而另一个派生类 D 同时从 B 和 C 继承。在这种情况下,D 类会有两份 A 类的副本,因为 B 和 C 各自继承了 A。这会导致数据冗余和函数调用的二义性。

1
2
3
4
5
    A
   / \
  B   C
   \ /
    D

1. 消除冗余:通过虚继承,派生类中只存在一份基类的实例,避免了数据冗余。
2. 解决二义性:由于只存在一份基类实例,函数调用和数据访问时不会出现二义性。
3. 效率影响:虚继承可能会增加一些开销,因为编译器需要维护额外的信息来确保正确的基类实例被访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
class A {
public:
    void show() {
        std::cout << "Class A" << std::endl;
    }
};
class B : virtual public A {
    // 虚继承 A
};
class C : virtual public A {
    // 虚继承 A
};
class D : public B, public C {
    // D 继承 B 和 C
};
int main() {
    D obj;
    obj.show(); // 调用 A 的方法,没有二义性
    return 0;
}

派生类可以访问基类的公共和受保护成员,但不能直接访问私有成员。可以通过公共或受保护的方法间接访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
private:
    int privateVar;
protected:
    int protectedVar;
public:
    int publicVar;
    void setPrivateVar(int val) {
        privateVar = val;
    }
};
class Derived : public Base {
public:
    void accessBaseMembers() {
        // privateVar; // 错误:不能访问私有成员
        protectedVar = 10// 可以访问受保护成员
        publicVar = 20// 可以访问公共成员
    }
};

函数遮蔽

函数遮蔽(Function Hiding)是指在派生类中定义一个与基类中同名但参数列表不同的函数,从而隐藏基类中的同名函数。这种机制允许派生类提供自己的实现,但会导致基类中同名函数的访问受到限制。

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
#include <iostream>
class Base {
public:
    void display() {
        std::cout << "Display from Base class" << std::endl;
    }
    void display(int x) {
        std::cout << "Display from Base class with integer: " << x << std::endl;
    }
};
class Derived : public Base {
public:
    // 遮蔽基类的 display 函数
    void display() {
        std::cout << "Display from Derived class" << std::endl;
    }
    // 重载 display 函数
    void display(double y) {
        std::cout << "Display from Derived class with double: " << y << std::endl;
    }
};
int main() {
    Derived obj;
    obj.display(); // 调用 Derived 类的 display
    obj.display(10); // 错误:调用 Base 类的 display(int),因为它被遮蔽
    obj.display(3.14); // 调用 Derived 类的 display(double)
    // 通过作用域解析运算符访问基类的 display
    obj.Base::display(); // 调用 Base 类的 display
    return 0;
}

怎么越过函数遮蔽

1. 使用作用域解析运算符
   你可以显式地调用基类的 display(int) 函数:

1
obj.Base::display(10); // 直接调用 Base 类的 display(int)

2. 在派生类中重载 display(int)
   如果你希望在 Derived 类中也能够处理 int 参数,可以在派生类中定义一个 display(int) 函数.

父类指针和子类指针

父类指针和子类指针的概念与对象的多态性和继承机制密切相关。使用父类指针可以指向子类对象,这使得可以通过父类接口来操作派生类对象,从而实现多态性。以下是对父类指针和子类指针的详细解释及示例。

父类指针:指向基类对象的指针。它可以指向基类的对象,也可以指向任何派生类的对象。
子类指针:指向派生类对象的指针。它只能指向该派生类的对象,不能指向基类的对象。

通过使用父类指针指向子类对象,可以实现多态性。多态性允许在运行时根据对象的实际类型来调用相应的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
class Base {
public:
    virtual void show() // 虚函数
        std::cout << "Base class show function." << std::endl;
    }
    virtual ~Base() {} // 虚析构函数
};
class Derived : public Base {
public:
    void show() override // 重写基类的虚函数
        std::cout << "Derived class show function." << std::endl;
    }
};
int main() {
    Base* basePtr; // 父类指针
    Derived derivedObj; // 子类对象
    basePtr = &derivedObj; // 父类指针指向子类对象
    basePtr->show(); // 调用派生类的 show 函数
    return 0;
}

子类指针只能指向该子类的对象,不能指向基类对象。

1
2
3
4
5
6
int main() {
    Derived derivedObj; // 子类对象
    Derived* derivedPtr = &derivedObj; // 子类指针
    derivedPtr->show(); // 调用 Derived 类的 show 函数
    return 0;
}

dynamic_cast可以安全的将父类指针转换为子类指针.

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>
class Base {
public:
    virtual ~Base() {}
};
class Derived : public Base {
public:
    void show() {
        std::cout << "Derived class show function." << std::endl;
    }
};
int main() {
    Base* basePtr = new Derived(); // 父类指针指向子类对象
    // 使用 dynamic_cast 安全地将父类指针转换为子类指针
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
        derivedPtr->show(); // 调用 Derived 类的 show 函数
    } else {
        std::cout << "Conversion failed." << std::endl;
    }
    delete basePtr; // 释放内存
    return 0;
}

纯虚函数

纯虚函数是一个没有实现的虚函数,它在基类中声明但不提供函数体。纯虚函数的目的是在派生类中强制实现该函数。纯虚函数的声明使用以下语法:

1
virtual void FunctionName() = 0;

在这个声明中,= 0表示这是一个纯虚函数。纯虚函数通常用于定义一个接口,要求所有派生类必须实现这个函数。

纯虚类

纯虚类是指至少包含一个纯虚函数的类。由于纯虚类不能实例化,因此它们主要用于提供一个抽象的接口,供其他类继承和实现。纯虚类的目的是为了定义一个统一的接口,而不提供具体的实现。

特点:

  1. 抽象性:纯虚类是抽象类,不能直接创建对象。
  2. 接口定义:它们用于定义接口,规定派生类必须实现的函数。
  3. 代码复用:通过继承纯虚类,不同的派生类可以有不同的实现,但都遵循相同的接口。

示例

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

class Derived : public AbstractBase {
public:
void pureVirtualFunction() override {
// 实现纯虚函数
std::cout << "Implemented pure virtual function in Derived class." << std::endl;
}
};

int main() {
// AbstractBase obj; // 错误:不能实例化纯虚类
Derived obj;
obj.pureVirtualFunction(); // 正确:调用派生类中实现的函数

return 0;
}

虚函数

允许通过基类指针或引用来调用派生类的重写函数,从而在运行时决定调用哪个函数。
只要有虚构函数的类应当申明虚构函数
没有虚析构函数的风险:如果 Base 的析构函数不是虚函数,删除 Derived 对象时只会调用 Base 的析构函数,而不会调用 Derived 的析构函数。这会导致 Derived 中的资源没有被正确释放。
设计原则:在设计类层次结构时,任何可能被继承的类都应该有一个虚析构函数,即使当前没有派生类。这样可以确保未来的扩展不会导致资源管理问题。

当一个派生类重写了基类的虚函数时,调用的是派生类中重写的版本,而不是基类的版本。这是因为虚函数机制旨在支持多态性,允许派生类提供特定于其自身的实现,这一点和构造函数,析构函数不一样.

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
#include <iostream>
class Shape {
public:
    virtual void draw() // 虚函数
        std::cout << "Drawing a generic shape." << std::endl;
    }
    virtual ~Shape() {} // 虚析构函数
};
class Circle : public Shape {
public:
    void draw() override // 重写基类的虚函数
        std::cout << "Drawing a circle." << std::endl;
    }
};
class Rectangle : public Shape {
public:
    void draw() override // 重写基类的虚函数,注意override关键字
        std::cout << "Drawing a rectangle." << std::endl;
    }
};
void renderShape(Shape* shape) {
    shape->draw(); // 调用虚函数
}
int main() {
    Circle circle;
    Rectangle rectangle;
    renderShape(&circle);    // 输出: Drawing a circle.
    renderShape(&rectangle);  // 输出: Drawing a rectangle.
    return 0;
}

运行时多态性:虚函数使得可以在运行时根据对象的实际类型来调用相应的函数。这种动态绑定允许实现灵活的代码结构。
接口的统一:通过基类定义虚函数,可以为派生类提供统一的接口。这使得可以使用基类指针或引用来操作不同类型的派生类对象。
代码的可扩展性:虚函数允许在不修改现有代码的情况下添加新类型的派生类。只需实现基类中定义的虚函数即可。
实现抽象类:虚函数可以用于定义抽象类(即包含至少一个纯虚函数的类),这使得可以强制派生类实现特定的功能

虚函数和函数遮蔽的区别

函数遮蔽与虚函数的多态性机制不同。函数遮蔽是一种静态绑定机制,编译器在编译时根据指针或引用的静态类型决定调用哪个函数,而不是在运行时根据对象的动态类型决定调用哪个函数。这意味着函数遮蔽不会实现动态绑定和多态性。

特性 虚函数 函数遮蔽
定义 在基类中声明为 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>
class Base {
public:
    void show() // 基类的 show 函数
        std::cout << "Show from Base class." << std::endl;
    }
};
class Derived : public Base {
public:
    void show() // 遮蔽基类的 show 函数
        std::cout << "Show from Derived class." << std::endl;
    }
};
void display(Base* b) {
    b->show(); // 静态绑定:调用 Base 的 show 函数
}
int main() {
    Base baseObj;
    Derived derivedObj;
    display(&baseObj);    // 输出: Show from Base class.
    display(&derivedObj); // 输出: Show from Base class.
    return 0;
}

代码解释

1. 基类Base 类定义了一个 show 函数。
2. 派生类Derived 类定义了一个与基类同名的 show 函数,这个函数遮蔽了基类中的 show 函数。
3. 调用函数
   - display(Base* b) 函数接受一个 Base 类型的指针,并调用 b->show()
   - 在 main 函数中,display(&baseObj) 和 display(&derivedObj) 都调用了基类的 show 函数,因为 display 函数的参数类型是 Base*,编译器在编译时决定调用 Base::show()
## 结果
静态绑定:由于函数遮蔽导致的静态绑定,display(&derivedObj) 调用的是 Base 类的 show 函数,而不是 Derived 类的 show 函数。
缺乏多态性:这种情况下,函数遮蔽无法实现多态性,因为调用的是基类的函数,而不是派生类的实现。
## 解决方案:使用虚函数
要实现动态绑定和多态性,应该使用虚函数。以下是如何改进代码以实现多态性:

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>
class Base {
public:
    virtual void show() // 虚函数
        std::cout << "Show from Base class." << std::endl;
    }
};
class Derived : public Base {
public:
    void show() override // 重写虚函数
        std::cout << "Show from Derived class." << std::endl;
    }
};
void display(Base* b) {
    b->show(); // 动态绑定:根据对象的动态类型调用相应的函数
}
int main() {
    Base baseObj;
    Derived derivedObj;
    display(&baseObj);    // 输出: Show from Base class.
    display(&derivedObj); // 输出: Show from Derived class.
    return 0;
}

结果

动态绑定:使用虚函数后,display(&derivedObj) 调用了 Derived 类的 show 函数,实现了多态性。
多态性:通过基类指针调用派生类的实现,实现了运行时的动态绑定。
虚函数是实现多态性的关键,而函数遮蔽由于其静态绑定的特性,无法实现这种行为。

迭代器示例

以下是一个使用标准模板库(STL)中的 std::vector 和迭代器的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>
int main() {
    // 创建一个整数向量
    std::vector<int> numbers = {12345};
    // 使用迭代器遍历向量
    std::vector<int>::iterator it;
    for (it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " "// 输出迭代器指向的元素
    }
    std::cout << std::endl;
    return 0;
}

代码解释

向量初始化:创建了一个 std::vector<int>,并初始化为 {1, 2, 3, 4, 5}
迭代器声明:使用 std::vector<int>::iterator 声明一个迭代器 it
遍历容器:通过 begin() 和 end() 方法获取向量的起始和结束迭代器,使用 for 循环遍历向量中的元素。
解引用:使用 *it 解引用迭代器以访问元素的值。

迭代器的常见问题

尽管迭代器为遍历容器提供了灵活性和一致性,但在使用迭代器时可能会遇到一些常见问题:
1. 失效迭代器(Dangling Iterator)
   - 当容器的结构发生变化(如插入或删除元素)时,某些迭代器可能会失效。
   - 例如,对于 std::vector,在插入或删除元素后,迭代器可能会失效,因为底层数组可能被重新分配。

1
2
3
4
std::vector<int> numbers = {12345};
auto it = numbers.begin();
numbers.insert(numbers.begin(), 0); // 插入元素导致迭代器失效
std::cout << *it << std::endl; // 未定义行为

2. 越界访问
   - 迭代器可能会被错误地用于越界访问容器。
   - 使用 end() 返回的迭代器是指向容器末尾之后的位置,解引用该迭代器是未定义行为。

1
2
3
std::vector<int> numbers = {12345};
auto it = numbers.end();
// std::cout << *it << std::endl; // 未定义行为:越界访问

3. 不匹配的迭代器类型
   - 在同一个容器中混用不同类型的迭代器(如常量迭代器和非常量迭代器)会导致编译错误或逻辑错误。

1
2
const std::vector<int> numbers = {12345};
std::vector<int>::iterator it = numbers.begin(); // 错误:不能使用非常量迭代器

4. 复杂性和性能问题
   - 对于某些容器(如 std::list),使用迭代器进行随机访问可能会导致性能问题,因为迭代器不支持随机访问。

迭代器常见问题

小心迭代器失效:在修改容器结构(如插入或删除元素)后,重新获取迭代器。
检查边界:确保迭代器在有效范围内使用,避免越界访问。
使用正确的迭代器类型:根据需要选择合适的迭代器类型(如常量迭代器)。
选择适合的容器:根据访问模式选择合适的容器类型,以优化性能。

友元

友元函数

友元函数是一个函数,它被授予访问某个类的私有和受保护成员的权限。友元函数不是类的成员函数,但可以访问该类的所有成员。

示例

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

class Box {
private:
double width;

public:
Box(double w) : width(w) {}

// 声明友元函数
friend void showWidth(const Box& b);
};

// 定义友元函数
void showWidth(const Box& b) {
std::cout << "Width: " << b.width << std::endl; // 访问私有成员
}

int main() {
Box box(10.5);
showWidth(box); // 调用友元函数

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
27
28
29
30
31
32
33
#include <iostream>

class Box; // 前向声明

class Display {
public:
// 声明 Box 类的友元成员函数
void showBoxWidth(const Box& b);
};

class Box {
private:
double width;

public:
Box(double w) : width(w) {}

// 将 Display::showBoxWidth 声明为友元成员函数
friend void Display::showBoxWidth(const Box& b);
};

// 定义友元成员函数
void Display::showBoxWidth(const Box& b) {
std::cout << "Width: " << b.width << std::endl; // 访问 Box 的私有成员
}

int main() {
Box box(15.0);
Display display;
display.showBoxWidth(box); // 调用友元成员函数

return 0;
}

3. 友元类

友元类是一个类,它被授予访问另一个类的所有私有和受保护成员的权限。友元类的所有成员函数都可以访问友元类的私有数据。

示例

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
#include <iostream>

class Box {
private:
double width;

public:
Box(double w) : width(w) {}

// 将 Display 类声明为友元类
friend class Display;
};

class Display {
public:
void showBoxWidth(const Box& b) {
std::cout << "Width: " << b.width << std::endl; // 访问 Box 的私有成员
}
};

int main() {
Box box(20.0);
Display display;
display.showBoxWidth(box); // 调用友元类的成员函数

return 0;
}

区别总结

  1. 友元函数:独立于类的普通函数,可以访问类的私有和受保护成员。需要在类中进行友元声明。
  2. 友元成员函数:另一个类的成员函数,可以访问类的私有和受保护成员。需要在类中进行友元声明。
  3. 友元类:整个类被授予访问另一个类的所有私有和受保护成员的权限。需要在类中进行友元声明。

使用注意

  • 友元破坏了类的封装性,因此应谨慎使用,只在必要时才授予友元权限。
  • 友元关系是单向的,即如果 A 是 B 的友元,B 并不是 A 的友元,除非显式声明。

为了提高程序的性能和资源管理效率,引入了移动构造函数和移动赋值运算符。这两个特性主要用于实现移动语义,允许对象的资源从一个对象“移动”到另一个对象,而不是进行昂贵的复制操作。这样可以显著提高程序在处理大型对象或容器时的效率。

移动构造函数(Move Constructor)

定义

移动构造函数用于从一个将亡值(xvalue)中“移动”资源到一个新的对象中,而不是复制资源。其典型定义如下:

1
ClassName(ClassName&& other);

其中,ClassName 是类的名称,other 是一个右值引用,表示即将被销毁的对象。

作用

  • 将资源(如动态分配的内存、文件句柄等)从 other 移动到正在构造的新对象。
  • other 的资源指针置为 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
25
26
27
28
29
30
31
#include <iostream>
#include <utility> // for std::move

class MyClass {
public:
int* data;

// 构造函数
MyClass(size_t size) : data(new int[size]) {
std::cout << "Constructed" << std::endl;
}

// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 防止释放已移动的资源
std::cout << "Move Constructed" << std::endl;
}

// 析构函数
~MyClass() {
delete[] data;
std::cout << "Destructed" << std::endl;
}
};

int main() {
MyClass obj1(10);
MyClass obj2 = std::move(obj1); // 调用移动构造函数

return 0;
}

移动赋值运算符(Move Assignment Operator)

定义

移动赋值运算符用于将资源从一个将亡值对象移动到另一个已存在的对象中。其典型定义如下:

1
ClassName& operator=(ClassName&& other);

作用

  • 释放当前对象持有的资源(如果有)。
  • 将资源从 other 移动到当前对象。
  • other 的资源指针置为 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class MyClass {
public:
int* data;

// 构造函数
MyClass(size_t size) : data(new int[size]) {
std::cout << "Constructed" << std::endl;
}

// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr;
std::cout << "Move Constructed" << std::endl;
}

// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data; // 释放当前对象的资源
data = other.data;
other.data = nullptr;
std::cout << "Move Assigned" << std::endl;
}
return *this;
}

// 析构函数
~MyClass() {
delete[] data;
std::cout << "Destructed" << std::endl;
}
};

int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = std::move(obj1); // 调用移动赋值运算符

return 0;
}

重要注意事项

  1. noexcept 关键字:移动构造函数和移动赋值运算符通常被标记为 noexcept,以便在某些标准库容器中使用时可以获得更好的性能。
  2. 资源管理:确保在移动操作后,源对象处于一个有效但未持有资源的状态,以避免资源泄漏和重复释放。
  3. 自我赋值检查:在移动赋值运算符中,通常需要检查自我赋值以避免不必要的操作。

通过使用移动构造函数和移动赋值运算符,可以显著提高程序的效率,特别是在处理大对象和容器时。

左值,右值,左值引用,右值引用

左值(lvalue)和右值(rvalue)是两个重要的概念,用于描述表达式在内存中的行为特性。这些概念帮助编译器理解如何管理内存和优化代码。

左值(lvalue)

  • 定义:左值是指那些有明确存储地址的表达式。它们通常表示可以被赋值的对象。
  • 特性
    • 可以出现在赋值运算符的左侧或右侧。
    • 通常是变量、数组元素、解引用指针等。
    • 具有持久的存储(即在表达式结束后依然存在)。
  • 示例
1
2
int x = 10;    // x 是一个左值
int *p = &x; // *p 是一个左值

右值(rvalue)

  • 定义:右值是指那些没有明确存储地址的临时值。它们通常表示不可以被赋值的对象。
  • 特性
    • 只能出现在赋值运算符的右侧。
    • 通常是字面量、临时对象、表达式的结果等。
    • 不具有持久的存储(即在表达式结束后可能被销毁)。
  • 示例
1
2
int y = x + 5; // x + 5 是一个右值
int z = 20; // 20 是一个右值

左值和右值的进一步细分

C++11引入了更细致的分类,包括将左值和右值进一步细分为:

  • 纯右值(prvalue):表示纯粹的临时值,例如字面量和返回的临时对象。
  • 将亡值(xvalue):表示即将被销毁的对象,例如通过 std::move 转换的对象。
  • 一般左值(glvalue):包括传统的左值和将亡值,表示具有地址的对象。
  • 可绑定左值(lvalue):传统的左值。
  • 右值(rvalue):包括纯右值和将亡值。

const引用是一个特殊的情况,它可以绑定到一个右值(即临时对象)。这是因为const引用不会允许通过该引用来修改对象,因此可以安全地绑定到临时对象。

左值引用(Lvalue Reference)

  • 定义:左值引用是指可以绑定到左值(即有持久存储地址的对象)的引用。
  • 语法:使用单个 & 符号。
  • 用途
    • 主要用于传递和操作函数参数,使得可以直接操作传入对象。
    • 可以避免对象的拷贝,提高效率。
  • 示例
1
2
3
int x = 10;
int &ref = x; // ref 是 x 的左值引用
ref = 20; // 修改 ref 会影响 x

右值引用(Rvalue Reference)

  • 定义:右值引用是指可以绑定到右值(即临时对象或将亡值)的引用。
  • 语法:使用双 && 符号。
  • 用途
    • 支持移动语义:允许开发者实现资源的转移而不是拷贝,从而提高程序性能。
    • 允许函数返回临时对象的引用,从而避免不必要的拷贝。
  • 示例
1
int &&rref = 5; // rref 是一个右值引用,绑定到临时右值 5

移动语义和右值引用的作用

右值引用在C++11中引入的一个重要用途是支持移动语义。移动语义允许开发者通过转移资源(如内存、文件句柄等)而不是复制资源来提高程序效率,特别是在处理大型对象或容器时。

移动构造函数和移动赋值运算符

  • 移动构造函数:接收一个右值引用作为参数,用于从另一个对象“移动”资源。
  • 移动赋值运算符:类似于移动构造函数,但用于赋值操作。

示例:

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>
#include <utility> // for std::move

class MyClass {
public:
int* data;

// 构造函数
MyClass(size_t size) : data(new int[size]) {
std::cout << "Constructed" << std::endl;
}

// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr;
std::cout << "Move Constructed" << std::endl;
}

// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
std::cout << "Move Assigned" << std::endl;
}
return *this;
}

// 析构函数
~MyClass() {
delete[] data;
std::cout << "Destructed" << std::endl;
}
};

int main() {
MyClass obj1(10);
MyClass obj2 = std::move(obj1); // 触发移动构造
MyClass obj3(5);
obj3 = std::move(obj2); // 触发移动赋值

return 0;
}