简介

  • C++ 相关的理论基础知识

C++ 结构体可以直接赋值吗

C++ 中的结构体(struct)可以进行直接赋值,但在赋值过程中会复制结构体的所有成员的值。这意味着一个结构体的内容可以复制到另一个结构体中,但它们之间是相互独立的,修改一个结构体的内容不会影响另一个结构体的内容。以下是一个简单的示例:

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>

struct Point {
    int x;
    int y;
};

int main() {
    Point p1 = {1, 2};
    Point p2;

    p2 = p1; // 直接赋值,将 p1 的内容复制到 p2

    std::cout << "p1.x: " << p1.x << " p1.y: " << p1.y << std::endl;
    std::cout << "p2.x: " << p2.x << " p2.y: " << p2.y << std::endl;

    // 修改 p1 不会影响 p2
    p1.x = 3;

    std::cout << "p1.x: " << p1.x << " p1.y: " << p1.y << std::endl;
    std::cout << "p2.x: " << p2.x << " p2.y: " << p2.y << std::endl;

    return 0;
}

在上面的示例中,首先创建了一个名为 p1 的结构体,并将其初始化为 {1, 2}。然后,通过 p2 = p1;p1 的内容复制到 p2,这样 p2 的内容就与 p1 相同。但之后修改 p1.x 的值不会影响 p2.x 的值,因为它们是相互独立的。

如果要实现结构体之间的浅拷贝(复制指向相同数据的指针或引用),或者实现自定义的赋值行为,您可能需要重载结构体的赋值运算符 =。这可以允许您自定义赋值的行为以满足特定的需求。

C++ 事件分发器 用类对象进行事件分发

在C++中,您可以创建一个事件分发器(Event Dispatcher)类,用于将事件分发给不同的事件处理对象(类对象)。以下是一个简单示例,演示如何使用类对象进行事件分发:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <vector>
#include <functional>

// 事件类型枚举
enum class EventType {
    MouseClick,
    MouseMove,
    KeyPress
};

// 事件数据结构
struct Event {
    EventType type;
    int x;
    int y;
    char key;
};

// 事件处理接口
class EventListener {
public:
    virtual void onEvent(const Event& event) = 0;
};

// 事件分发器
class EventDispatcher {
public:
    void addListener(EventListener* listener) {
        listeners.push_back(listener);
    }

    void dispatchEvent(const Event& event) {
        for (auto listener : listeners) {
            listener->onEvent(event);
        }
    }

private:
    std::vector<EventListener*> listeners;
};

// 具体的事件处理类
class MouseEventListener : public EventListener {
public:
    void onEvent(const Event& event) override {
        if (event.type == EventType::MouseClick) {
            std::cout << "Mouse clicked at (" << event.x << ", " << event.y << ")" << std::endl;
        } else if (event.type == EventType::MouseMove) {
            std::cout << "Mouse moved to (" << event.x << ", " << event.y << ")" << std::endl;
        }
    }
};

class KeyPressListener : public EventListener {
public:
    void onEvent(const Event& event) override {
        if (event.type == EventType::KeyPress) {
            std::cout << "Key pressed: " << event.key << std::endl;
        }
    }
};

int main() {
    EventDispatcher dispatcher;
    MouseEventListener mouseListener;
    KeyPressListener keyListener;

    dispatcher.addListener(&mouseListener);
    dispatcher.addListener(&keyListener);

    // 模拟事件触发
    Event mouseEvent = {EventType::MouseClick, 100, 200, '\0'};
    Event keyEvent = {EventType::KeyPress, 0, 0, 'A'};

    dispatcher.dispatchEvent(mouseEvent);
    dispatcher.dispatchEvent(keyEvent);

    return 0;
}

在这个示例中,我们首先定义了事件类型枚举(EventType)和事件数据结构(Event),以便表示不同类型的事件及其相关数据。然后,我们创建了一个事件监听接口(EventListener),它包含一个抽象的 onEvent 函数,用于处理事件。

接下来,我们创建了一个事件分发器类(EventDispatcher),它可以注册事件监听器并通过 dispatchEvent 函数将事件分发给已注册的监听器。

最后,我们创建了两个具体的事件监听器类(MouseEventListenerKeyPressListener),它们分别实现了 onEvent 函数来处理不同类型的事件。

main 函数中,我们创建了事件分发器对象 dispatcher,并将两个事件监听器对象注册到分发器中。然后,我们模拟事件的触发,创建事件对象并通过分发器将事件发送给相应的监听器。

这种方式使事件处理和事件分发能够更加灵活,不同的监听器可以独立实现它们自己的事件处理逻辑,从而使代码更加模块化和可扩展。

C++ 事件处理

在C++中进行事件处理通常涉及以下几个方面:

  1. 事件类型定义: 首先,您需要定义表示不同事件类型的数据结构或枚举。这些事件类型通常包括鼠标点击、键盘输入、定时器触发等。

  2. 事件监听器: 事件监听器是负责监听和处理特定事件类型的类。每个事件类型都应该有一个相应的事件监听器。事件监听器包含了事件处理函数,当事件发生时,这些函数会被调用。

  3. 事件分发器: 事件分发器是一个中介者,负责将事件分发给正确的事件监听器。它通常包含一个事件队列,用于存储待处理的事件,以及一个事件分发函数,用于将事件发送到相应的监听器。

以下是一个简单的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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <vector>
#include <functional>

// 定义鼠标事件类型
enum class MouseEventType {
    Click,
    Move,
    Scroll
};

// 鼠标事件监听器接口
class MouseEventListener {
public:
    virtual void onMouseEvent(MouseEventType type, int x, int y) = 0;
};

// 鼠标事件分发器
class MouseEventDispatcher {
public:
    // 注册鼠标事件监听器
    void addListener(MouseEventListener* listener) {
        listeners.push_back(listener);
    }

    // 模拟鼠标事件触发
    void simulateMouseEvent(MouseEventType type, int x, int y) {
        for (auto listener : listeners) {
            listener->onMouseEvent(type, x, y);
        }
    }

private:
    std::vector<MouseEventListener*> listeners;
};

// 具体的鼠标事件监听器
class ClickListener : public MouseEventListener {
public:
    void onMouseEvent(MouseEventType type, int x, int y) override {
        if (type == MouseEventType::Click) {
            std::cout << "Mouse Clicked at (" << x << ", " << y << ")" << std::endl;
        }
    }
};

class MoveListener : public MouseEventListener {
public:
    void onMouseEvent(MouseEventType type, int x, int y) override {
        if (type == MouseEventType::Move) {
            std::cout << "Mouse Moved to (" << x << ", " << y << ")" << std::endl;
        }
    }
};

int main() {
    MouseEventDispatcher dispatcher;
    ClickListener clickListener;
    MoveListener moveListener;

    dispatcher.addListener(&clickListener);
    dispatcher.addListener(&moveListener);

    // 模拟鼠标事件触发
    dispatcher.simulateMouseEvent(MouseEventType::Click, 100, 200);
    dispatcher.simulateMouseEvent(MouseEventType::Move, 150, 250);

    return 0;
}

在这个示例中,我们首先定义了鼠标事件类型(MouseEventType)和鼠标事件监听器接口(MouseEventListener)。然后,我们创建了一个鼠标事件分发器(MouseEventDispatcher),它可以注册监听器,并通过simulateMouseEvent函数模拟鼠标事件的触发。最后,我们创建了两个具体的鼠标事件监听器(ClickListenerMoveListener),并将它们注册到事件分发器中。

当事件分发器调用simulateMouseEvent时,它会将事件分发给注册的监听器,监听器会根据事件类型执行相应的操作。

这是一个非常简单的事件处理示例,实际的事件处理系统可能更加复杂,具体取决于您的应用程序需求。但这个示例可以帮助您理解事件处理的基本概念和实现方式。

C++ 设计模式 分发表

在C++中,使用分发表(Dispatch Table)设计模式可以根据特定的输入键(如字符串或整数)来选择并执行不同的函数或操作。这种模式在实际编程中经常用于创建插件系统、命令模式、事件处理等场景,其中需要根据某种标识来动态选择和执行不同的功能。下面是一个简单的示例,演示如何使用分发表设计模式。

首先,我们需要定义一个抽象基类,以及一组不同的子类,每个子类代表一个具体的操作或函数。然后,我们使用分发表将这些子类与特定的输入键相关联,以便根据输入键来执行相应的操作。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <unordered_map>
#include <functional>

// 抽象基类
class Operation {
public:
    virtual void execute() = 0;
};

// 具体操作类
class OperationA : public Operation {
public:
    void execute() override {
        std::cout << "OperationA executed." << std::endl;
    }
};

class OperationB : public Operation {
public:
    void execute() override {
        std::cout << "OperationB executed." << std::endl;
    }
};

class OperationC : public Operation {
public:
    void execute() override {
        std::cout << "OperationC executed." << std::endl;
    }
};

// 分发表类
class DispatchTable {
public:
    using OperationCreator = std::function<Operation*()>;

    void registerOperation(const std::string& key, OperationCreator creator) {
        operationTable[key] = creator;
    }

    Operation* createOperation(const std::string& key) {
        if (operationTable.find(key) != operationTable.end()) {
            return operationTable[key]();
        }
        return nullptr;
    }

private:
    std::unordered_map<std::string, OperationCreator> operationTable;
};

int main() {
    DispatchTable dispatchTable;

    // 注册具体操作类和对应的创建函数
    dispatchTable.registerOperation("A", []() { return new OperationA(); });
    dispatchTable.registerOperation("B", []() { return new OperationB(); });
    dispatchTable.registerOperation("C", []() { return new OperationC(); });

    // 根据输入键创建并执行操作
    Operation* opA = dispatchTable.createOperation("A");
    if (opA) {
        opA->execute();
        delete opA;
    }

    Operation* opB = dispatchTable.createOperation("B");
    if (opB) {
        opB->execute();
        delete opB;
    }

    Operation* opX = dispatchTable.createOperation("X");
    if (!opX) {
        std::cout << "Operation not found." << std::endl;
    }

    return 0;
}

在上述示例中,我们首先定义了一个抽象基类 Operation,以及三个具体操作类 OperationAOperationBOperationC。然后,我们创建了一个 DispatchTable 类,它可以注册不同的操作类和相应的创建函数,并通过输入键来动态创建和执行不同的操作。

这种分发表设计模式允许您根据特定的输入来选择和执行不同的操作,从而使程序更加灵活和可扩展。

C++ 分发表 策略模式 详解

在C++中,分发表(Dispatch Table)和策略模式(Strategy Pattern)是两种常用的设计模式,用于处理不同情况下的分发和决策。下面详细解释这两个模式以及如何在C++中实现它们:

分发表(Dispatch Table)

分发表是一种数据结构,通常是一个数组或映射,它将函数指针或对象与不同的操作或事件相关联。这可以用于根据某些条件选择执行不同的操作。分发表通常用于处理消息传递、事件处理和动态行为定义等情况。

如何实现分发表:

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

class Dispatcher {
public:
    using FunctionType = std::function<void()>;

    void Register(const std::string& key, FunctionType func) {
        dispatchTable[key] = func;
    }

    void Dispatch(const std::string& key) {
        if (dispatchTable.find(key) != dispatchTable.end()) {
            dispatchTable[key]();
        } else {
            std::cout << "Function not found for key: " << key << std::endl;
        }
    }

private:
    std::unordered_map<std::string, FunctionType> dispatchTable;
};

int main() {
    Dispatcher dispatcher;
    
    dispatcher.Register("Function1", []() {
        std::cout << "Function1 called." << std::endl;
    });

    dispatcher.Register("Function2", []() {
        std::cout << "Function2 called." << std::endl;
    });

    dispatcher.Dispatch("Function1"); // Calls Function1
    dispatcher.Dispatch("Function2"); // Calls Function2
    dispatcher.Dispatch("Function3"); // Function not found
    
    return 0;
}

在上面的示例中,我们创建了一个Dispatcher类,它使用std::unordered_map来存储不同函数的函数指针。然后,我们使用Register函数注册不同的函数,并使用Dispatch函数来根据给定的键调用相应的函数。

策略模式

策略模式是一种行为设计模式,它允许您在运行时选择算法或行为,而不需要修改客户端代码。在策略模式中,不同的策略封装为不同的类,客户端代码可以在运行时选择使用哪种策略。

如何实现策略模式:

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

// Strategy interface
class PaymentStrategy {
public:
    virtual void pay(int amount) = 0;
};

// Concrete strategies
class CreditCardPayment : public PaymentStrategy {
public:
    void pay(int amount) override {
        std::cout << "Paid " << amount << " using credit card." << std::endl;
    }
};

class PayPalPayment : public PaymentStrategy {
public:
    void pay(int amount) override {
        std::cout << "Paid " << amount << " using PayPal." << std::endl;
    }
};

// Context that uses the strategy
class ShoppingCart {
public:
    ShoppingCart(PaymentStrategy* paymentStrategy) : paymentStrategy(paymentStrategy) {}

    void checkout(int amount) {
        paymentStrategy->pay(amount);
    }

private:
    PaymentStrategy* paymentStrategy;
};

int main() {
    CreditCardPayment creditCard;
    PayPalPayment payPal;

    ShoppingCart cart1(&creditCard);
    ShoppingCart cart2(&payPal);

    cart1.checkout(100);
    cart2.checkout(50);

    return 0;
}

在上面的示例中,我们定义了一个PaymentStrategy接口,具体的支付策略(CreditCardPaymentPayPalPayment)实现了这个接口。然后,ShoppingCart类使用策略接口来完成支付,客户端代码可以在运行时选择不同的支付策略,而不需要修改ShoppingCart的代码。

这两种模式都有助于将不同行为解耦,提高代码的可维护性和可扩展性。分发表用于根据键分发不同操作,而策略模式用于根据需要选择不同的算法或行为。您可以根据具体需求选择使用哪种模式,或者在某些情况下,它们可以结合使用以实现更复杂的逻辑。

stdint.h 详解

stdint.h 是 C 语言标准库中的一个头文件,它提供了一些固定大小的整数类型,以增强代码的可移植性和可读性。在不同的平台上,整数类型的大小可能会有所不同,为了确保代码在各种环境下都能正常工作,可以使用 stdint.h 中定义的整数类型。

以下是 stdint.h 中常见定义的解释:

  1. int8_t、int16_t、int32_t、int64_t: 这些是有符号整数类型,分别表示 8 位、16 位、32 位和 64 位的整数。它们的范围是从 -2^n-12^n-1 - 1,其中 n 是整数类型的位数。

  2. uint8_t、uint16_t、uint32_t、uint64_t: 这些是无符号整数类型,与上面的有符号整数类型类似,分别表示 8 位、16 位、32 位和 64 位的无符号整数。它们的范围是从 0 到 2^n - 1

  3. int_least8_t、int_least16_t、int_least32_t、int_least64_t: 这些是至少具有指定位数的有符号整数类型。它们可以更具灵活性地选择整数类型,以适应不同平台的位数要求。

  4. uint_least8_t、uint_least16_t、uint_least32_t、uint_least64_t: 类似于上面的 int_leastX_t 类型,这些是至少具有指定位数的无符号整数类型。

  5. int_fast8_t、int_fast16_t、int_fast32_t、int_fast64_t: 这些是最快的有符号整数类型,可以根据平台的性能选择合适的整数类型,以提高性能。

  6. uint_fast8_t、uint_fast16_t、uint_fast32_t、uint_fast64_t: 类似于上面的 int_fastX_t 类型,这些是最快的无符号整数类型。

  7. intptr_t: 这是一个有符号整数类型,足够大以容纳指针值。它的大小可以保证足够存储指针。

  8. uintptr_t: 类似于上面的 intptr_t 类型,这是一个无符号整数类型,足够大以容纳指针值。

通过使用 stdint.h 中定义的整数类型,你可以编写更加可移植且具有明确整数大小的代码。这在需要确保整数大小和跨平台一致性的情况下非常有用,尤其是在涉及底层编程、二进制数据处理和嵌入式系统等领域。

stdlib.h 详解

stdlib.h 是 C 语言标准库中的一个头文件,它提供了一些与内存分配、随机数生成、字符串转换等功能相关的函数和定义。这个头文件包含了许多常用的函数,能够帮助你进行内存管理、随机数生成以及字符串处理等操作。

以下是 stdlib.h 中常见函数和定义的解释:

  1. malloc(size_t size): 这个函数用于在堆内存上分配一块指定大小的内存空间,并返回一个指向该内存块的指针。如果分配成功,返回的指针可以用来存储数据。如果分配失败,返回 NULL

  2. calloc(size_t num_elements, size_t element_size): 类似于 malloc,但是它会分配一块可以存储多个元素的内存空间,并将这些内存初始化为零。它接受两个参数,一个是元素数量,另一个是每个元素的大小。

  3. realloc(void *ptr, size_t new_size): 这个函数允许你重新调整之前使用 malloccalloc 分配的内存块的大小。它接受一个指针和新的大小作为参数,会尝试重新分配内存,并返回一个新的指向调整后内存块的指针。如果调整失败,返回 NULL

  4. free(void *ptr): 用于释放之前通过 malloccallocrealloc 分配的内存空间。释放后,这块内存可以被重新分配给其他数据。

  5. rand()srand(unsigned int seed): rand() 用于生成一个伪随机整数,范围通常在 0 到 RAND_MAX 之间。srand() 用于初始化随机数生成器的种子,以便每次程序运行时生成不同的随机序列。

  6. atoi(const char *str)atof(const char *str): 这些函数用于将字符串转换为整数或浮点数。

  7. exit(int status): 这个函数终止程序的执行,并返回一个状态码给操作系统。通常情况下,0 表示程序成功执行,非零值表示程序异常终止。

  8. system(const char *command): 这个函数允许你执行系统命令。它会调用操作系统的命令解释器来执行给定的命令字符串。

  9. getenv(const char *name): 这个函数用于获取环境变量的值,返回一个指向字符串的指针。

stdlib.h 提供了许多常用的函数,用于内存分配、随机数生成和字符串转换等操作。这些函数可以帮助你写出更加功能完备且可移植的 C 代码。记住,在使用这些函数时要小心内存泄漏等问题,确保在不需要使用分配的内存时进行释放。

stddef.h 详解

stddef.h 是 C 语言标准库中的一个头文件,它提供了一些与指针和数据类型大小相关的定义。它为了提供可移植性和规范性,定义了一些常用的宏和类型,使得代码在不同平台上能够保持一致性。

以下是一些 stddef.h 中常见定义的解释:

  1. NULL: 这是一个用来表示空指针的宏,通常被定义为整数 0。在 C 语言中,空指针表示指向任何内存位置的无效指针。

  2. size_t: 这是一个无符号整数类型,在很多情况下用来表示内存块的大小、数组的长度等。它的大小根据不同的系统架构可能会有所变化。

  3. ptrdiff_t: 这是一个用来表示两个指针之间差值的整数类型。它在指针运算和数组索引中很有用。

  4. offsetof(type, member): 这是一个宏,用来计算一个结构体中成员的偏移量。在某些情况下,你可能需要直接访问结构体内部的某个成员,这时就可以用这个宏来计算偏移量。

  5. wchar_t: 这是一种用来表示宽字符的数据类型,通常用在支持多语言字符集的环境中。

总的来说,stddef.h 提供了一些基本的数据类型和宏定义,有助于编写更加通用和可移植的 C 代码。通过使用这些定义,你可以在不同的系统上编写出更具可读性和健壮性的代码,避免了硬编码的问题。记住,这些定义可能会在不同的编译器和系统中有所不同,所以在使用时要注意兼容性。

sizeof 详解

在C++中,sizeof是一元运算符,用于确定数据类型或对象的大小(以字节为单位)。它返回一个size_t(无符号整数)值,表示操作数的大小。

sizeof主要有两种用法:

  1. 数据类型的大小: 可以使用sizeof来获取数据类型的大小。例如:
    1
    2
    3
    
    sizeof(int);     // 返回int类型的大小(通常为4字节)
    sizeof(char);    // 返回char类型的大小(通常为1字节)
    sizeof(double);  // 返回double类型的大小(通常为8字节)
    
  2. 对象的大小: 还可以使用sizeof来获取对象在内存中的大小。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    int arr[5];
    sizeof(arr);       // 返回整个数组的大小(5 * sizeof(int))
       
    struct MyStruct {
        int x;
        char c;
    };
       
    MyStruct obj;
    sizeof(obj);       // 返回整个MyStruct对象的大小
    

需要注意的是,sizeof在编译时求值,它不会实际执行给定的表达式。它在处理数组、结构体或需要使用mallocnew动态分配内存时特别有用。

值得一提的是,某些数据类型的大小可能因平台和使用的编译器而异。然而,sizeof保证在编译时准确确定数据类型或对象的大小,从而确保在不同系统上的可移植性。

强关联 是什么

“强关联”(Strong Association)是面向对象编程(OOP)中的一个概念,指的是两个或多个类之间的关系非常紧密,彼此之间的依赖性很强。

在强关联关系中,一个类通常直接使用另一个类的成员变量或方法,或者一个类继承自另一个类。这意味着两个类之间的交互非常频繁和紧密,一个类的变化可能会直接影响到另一个类。

强关联关系通常表示了一种较强的功能或逻辑上的依赖关系。这种关系可能是必需的,因为一个类的功能需要另一个类提供的支持或协助。然而,强关联关系也可能会导致较高的耦合性,从而使代码的可维护性和可扩展性受到一定影响。

与强关联相对的概念是弱关联(Weak Association),指的是两个类之间的依赖性较弱,彼此之间的交互相对较少或较松散。在弱关联关系中,一个类可能通过参数传递、接口或消息传递等方式与另一个类进行交互,而不是直接访问其成员变量或方法。

理解类之间的关联类型对于设计和编写高质量的面向对象代码非常重要。在软件开发中,要根据具体的需求和设计目标,合理选择和管理类之间的关联关系,以确保代码的可读性、可维护性和可扩展性。


队列 应用场景

C++队列(Queue)是一种先进先出(FIFO)的数据结构,它支持在一端插入元素(入队),在另一端删除元素(出队)。队列在许多应用场景中非常有用,以下是一些常见的应用场景:

  1. 任务调度:队列可以用于任务调度,其中待处理的任务按照其到达时间的顺序排队。每当一个任务完成,可以从队列的前端移除任务,然后处理下一个任务。

  2. 缓冲区管理:队列可用于缓冲区管理,其中生产者将数据插入队列的末尾,而消费者从队列的前端取出数据进行处理。这样可以平衡生产者和消费者之间的速度差异。

  3. 广度优先搜索:队列常用于广度优先搜索算法(BFS),该算法在图或树结构中逐层扩展搜索。每当从队列的前端取出一个元素时,将其相邻的未访问节点插入队列的末尾,以便在下一层进行进一步的搜索。

  4. 消息传递:队列可用于在不同的线程或进程之间传递消息。一个线程将消息插入队列的末尾,而另一个线程从队列的前端取出消息并进行处理。

  5. 系统调度:操作系统中的任务调度器通常使用队列来管理进程的执行顺序。进程按照优先级或其他调度策略排队等待执行。

  6. 打印任务队列:在打印机管理中,队列可以用于存储待打印的文档。每当一个文档完成打印,下一个文档可以从队列的前端取出。

这些只是一些队列的应用场景示例,实际上队列在许多领域中都有广泛的应用。队列提供了一种有序的数据处理方式,可以有效地管理和处理数据流。


sqlpp11 详解

sqlpp11 是一个 C++ 编程库,提供了一种类型安全的 SQL 查询构建和执行方式。它使用 C++ 的强类型系统和模板元编程技术,允许开发者以类型安全的方式构建 SQL 查询,同时提供了对多种关系型数据库的支持。

以下是关于 sqlpp11 的一些详细说明:

1. 类型安全的 SQL 查询构建: sqlpp11 允许开发者使用 C++ 类型和表达式来构建 SQL 查询,这种方式可以在编译时捕获错误和类型不匹配,并提供更好的代码安全性和可维护性。

2. 支持多种关系型数据库: sqlpp11 支持多种常见的关系型数据库,如 MySQL、PostgreSQL、SQLite 和 Microsoft SQL Server 等,使开发者可以使用相同的接口和查询语法来操作不同的数据库系统。

3. 查询表达式和条件: sqlpp11 提供了丰富的查询表达式和条件,包括等于、不等于、小于、大于、逻辑运算符等,以及通用函数和聚合函数等,使开发者可以构建复杂的查询条件和数据转换。

4. 插入、更新和删除数据: sqlpp11 提供了方便的接口和语法来执行插入、更新和删除数据的操作,使开发者可以通过简单的代码调用来完成数据库操作。

5. 事务支持: sqlpp11 支持数据库事务的操作,允许开发者在事务中执行一系列的数据库操作,并保证数据的一致性和完整性。

6. 数据库迁移支持: sqlpp11 提供了数据库迁移的支持,允许开发者定义数据库表结构的变化和版本控制,以方便数据库结构的升级和维护。

使用 sqlpp11,开发者可以在 C++ 程序中以一种类型安全的方式构建和执行 SQL 查询,而无需手动编写 SQL 语句。这样可以减少错误和调试的工作量,提高代码的可读性和可维护性。然而,使用 sqlpp11 仍然需要对关系型数据库和 SQL 语法有一定的了解,以便正确地构建查询和操作数据库。


ORM 是什么

ORM 是对象关系映射(Object-Relational Mapping)的缩写,它是一种编程技术或模式,用于在关系型数据库和面向对象编程语言之间建立映射关系。ORM 提供了一种将数据库中的数据映射为编程语言中的对象的方法,同时也允许开发者通过操作对象来操作数据库,而不必直接编写 SQL 查询语句。

ORM 的主要目标是简化数据库操作和数据持久化的过程,以提高开发效率和代码的可维护性。它使开发者可以使用面向对象的方式处理数据,而无需关注数据库的细节和复杂的 SQL 语法。

ORM 通常提供以下功能和特性:

  1. 对象和表之间的映射:ORM 将数据库中的表和记录映射为编程语言中的对象和属性,使开发者可以通过操作对象来进行数据库操作。

  2. 数据库查询和操作:ORM 提供了一组方法和接口,用于执行数据库查询、插入、更新和删除等操作,隐藏了底层数据库操作的复杂性。

  3. 数据关系处理:ORM 可以处理对象之间的关系,如一对一、一对多和多对多关系,并提供方便的方法来处理这些关系。

  4. 数据校验和验证:ORM 提供了验证规则和约束,用于验证对象数据的完整性和有效性,确保数据的一致性和准确性。

  5. 数据库迁移和版本管理:ORM 通常提供数据库迁移工具,用于管理数据库结构的变化和版本控制。

ORM 在不同的编程语言和框架中有多种实现,如在 Python 中有 SQLAlchemy、Django ORM;在 Java 中有 Hibernate、MyBatis;在 .NET 中有 Entity Framework 等。

ORM 可以简化开发人员与数据库之间的交互,减少了手动编写 SQL 语句的工作量,提高了开发效率。然而,在使用 ORM 时,仍然需要了解数据库的基本概念和 SQL 查询语句,以便理解和优化生成的查询语句,以及处理复杂的数据库操作。


1970年1月1日以来的时间戳称为什么时间

在计算机科学中,1970年1月1日以来的时间戳被称为UNIX时间戳(Unix Timestamp),也称为Epoch时间。

UNIX时间戳是一种表示时间的方式,它以秒为单位计量时间,从协调世界时(UTC)的1970年1月1日午夜(零点)开始计算。UNIX时间戳记录了自那个时间点起经过的秒数。

UNIX时间戳在计算机系统中广泛使用,特别是在UNIX、Linux和类UNIX操作系统中。它被用作计算和记录时间、日期的标准方法,也被广泛应用于时间相关的编程和系统操作。

通过UNIX时间戳,我们可以方便地进行时间计算、时间戳的转换和比较,而不受时区和日期格式的影响。许多编程语言和操作系统提供了相关的函数和工具,用于将UNIX时间戳转换为日期和时间,以及进行时间戳的操作和处理。

需要注意的是,UNIX时间戳是基于UTC的,不考虑夏令时(Daylight Saving Time)或其他时区调整。如果需要考虑时区差异,可能需要进行额外的时区转换和调整操作。


linux sysroot 是什么

在Linux中,sysroot是指用于交叉编译的根文件系统(root filesystem)。它是一个包含目标平台上的系统库、头文件和其他依赖项的目录。

当进行交叉编译时,您通常需要访问目标平台的系统库和头文件,以确保编译的代码与目标平台上的运行时环境兼容。这些系统库和头文件包含了特定于目标平台的API定义、标准库函数等。

sysroot提供了一个用于交叉编译的独立环境,使您能够在开发主机上使用目标平台的系统库和头文件。通过将sysroot指定为交叉编译工具链的一部分,编译器和其他工具将在sysroot中查找目标平台的依赖项。

在交叉编译工具链的配置中,您可以指定sysroot的路径,以便编译器在编译过程中正确地引用目标平台的系统库和头文件。这样,您就可以编译适用于目标平台的应用程序。

sysroot的结构类似于目标平台的根文件系统结构,但通常只包含必要的系统库和头文件,而不是完整的根文件系统。它是一个与目标平台相关的特定目录,可能包含像usr/includeusr/lib等子目录。

通过使用sysroot,您可以确保交叉编译的代码与目标平台上的运行环境保持一致,并且可以在开发主机上进行测试和构建,而无需实际部署到目标设备上。


linux arm-linux-gnueabihf 是什么

arm-linux-gnueabihf 是一个用于ARM架构的交叉编译工具链。让我们逐个解释每个组成部分的含义:

  • arm: 表示目标架构是ARM。ARM是一种广泛用于嵌入式系统和移动设备的处理器架构。

  • linux: 表示目标操作系统是Linux。这意味着您可以使用该工具链构建在Linux上运行的应用程序。

  • gnueabihf: 这是一组ABI(Application Binary Interface)和硬浮点(hard float)的选项。

    • ABI:ABI定义了应用程序与操作系统之间的接口规范,包括函数调用约定、寄存器用途和内存布局等。在这种情况下,gnueabi表示使用GNU工具链的ARM嵌入式ABI。

    • 硬浮点:hf表示硬浮点。在ARM架构中,浮点运算可以使用软件模拟实现(软浮点)或使用硬件浮点单元(硬浮点)。gnueabihf表示使用硬浮点,这意味着浮点运算将由ARM处理器的硬件浮点单元执行,而不是通过软件模拟。

因此,arm-linux-gnueabihf工具链适用于在ARM架构上运行Linux操作系统的应用程序的交叉编译。它提供了必要的编译器、链接器和库文件等工具,以便您可以在开发主机上构建适用于ARM架构的应用程序。


Linux 交叉编译

在Linux上进行交叉编译是指在一个操作系统上开发和构建适用于另一个不同架构或操作系统的软件。这通常用于在一台主机上构建用于嵌入式设备、嵌入式系统或不同操作系统的应用程序。

以下是进行Linux交叉编译的一般步骤:

  1. 安装交叉编译工具链:首先,您需要安装适用于目标架构的交叉编译工具链。工具链包括交叉编译器、链接器和库文件等。通常,工具链的名称以目标平台的架构开头,如arm-linux-gnueabi-。

  2. 配置环境变量:将交叉编译工具链添加到系统的PATH环境变量中。这样,您可以在命令行中直接使用交叉编译器和其他工具。

  3. 配置构建系统:如果您正在使用自动构建系统(如Makefile或CMake),请确保将构建系统配置为使用交叉编译工具链。这包括设置正确的编译器、链接器和其他构建选项。

  4. 编写代码:编写适用于目标平台的代码。这可能涉及到使用特定于目标平台的API、库或头文件等。

  5. 进行交叉编译:使用交叉编译工具链编译和链接您的代码。例如,如果您使用C语言编写代码,可以使用交叉编译器来编译源文件并生成目标平台的可执行文件。

以下是一个简单的示例,演示如何在Linux上进行交叉编译:

  1. 假设您要交叉编译一个适用于ARM架构的应用程序。

  2. 安装ARM架构的交叉编译工具链,例如arm-linux-gnueabi-gcc、arm-linux-gnueabi-ld等。您可以从交叉编译工具链的官方网站或Linux发行版的软件包管理器中获取。

  3. 配置环境变量,将交叉编译工具链添加到PATH中。例如,在bash shell中,您可以执行以下命令:

1
export PATH=/path/to/cross-compiler/bin:$PATH

确保将/path/to/cross-compiler/bin替换为您安装交叉编译工具链的实际路径。

  1. 编写适用于ARM架构的代码,保存为main.c文件:
1
2
3
4
5
6
#include <stdio.h>

int main() {
    printf("Hello, ARM!\n");
    return 0;
}
  1. 使用交叉编译器编译代码:
1
arm-linux-gnueabi-gcc -o hello_arm main.c

这将使用交叉编译器生成一个名为hello_arm的可执行文件,该文件适用于ARM架构的设备。


public 关键字

在C++中,public是一种访问修饰符(Access Modifier),用于定义类的成员的访问权限。public成员在类的内部和外部都可见,可以被任何对象或函数访问。

以下是一些关键点来详细解释public的使用和行为:

  1. public成员在类内部和外部可见:public成员可以在类内部的成员函数中访问,也可以在类的外部通过对象或类名加点操作符(.)进行访问。

  2. public成员可以被任何对象或函数访问:public成员可以被类的对象、其他对象或函数直接访问。这使得它们可以作为类的接口提供给外部使用。

  3. public成员没有访问限制:public成员在类的内外部没有访问限制,可以被任意代码使用和修改。

  4. public继承:当一个类以public方式继承另一个类时,基类的public成员在派生类中仍然是public成员,可以直接访问。

  5. public与封装性:public成员违反了类的封装性,因为它们可以被外部直接访问和修改。封装性的目标是将数据和操作封装在类的内部,通过类的接口进行访问。

在C++中,使用public修饰符来声明类的成员是一种常见的做法,特别是用于定义类的公共接口。公共接口定义了类对外提供的功能和数据,对外部代码是可见和可访问的,用于与类进行交互和操作。通过将公共成员放在public部分,可以明确地表明这些成员是供外部使用的。

尽管public成员在类的封装性方面存在一些限制,但它们在许多情况下是必需的,特别是当需要公开某些功能或数据时。在设计类时,需要仔细考虑哪些成员应该是public的,以确保良好的封装性和代码安全性。


protected 关键字

在C++中,protected是一种访问修饰符(Access Modifier),用于定义类的成员的访问权限。protected成员在类内部和派生类中可见,但在类的外部不可直接访问。

当类的成员被声明为protected时,它们可以在同一类的成员函数中访问,并且可以在该类的派生类中进行访问。这意味着派生类可以继承并访问基类的protected成员。而对于类的外部,无法直接访问protected成员。

以下是一些关键点来详细解释protected的使用和行为:

  1. protected成员在派生类中可见:当一个类被继承时,派生类可以访问基类中声明为protected的成员变量和成员函数。

  2. protected成员在类内部可见:类内部的成员函数可以访问类中的所有成员,包括protected成员。

  3. protected成员无法在类的外部直接访问:类的外部无法直接访问类的protected成员变量和成员函数。只有通过类的公共成员函数或在派生类中进行间接访问。

  4. protected继承:当一个类以protected方式继承另一个类时,基类的publicprotected成员在派生类中都变为protected成员,而基类的private成员在派生类中仍然是不可访问的。

  5. protectedprivate的区别:protected成员在派生类中可见,而private成员在派生类中不可见。只有类内部可以访问private成员,而类内部和派生类中都可以访问protected成员。

使用protected修饰符可以实现类的封装性和继承性的结合。它可以保护类的成员不被直接访问和修改,同时又允许派生类继承和访问这些成员,提供了更灵活的访问控制方式。


private 关键字

在C++中,private是一种访问修饰符(Access Modifier),用于定义类的成员的访问权限。private成员在类的内部可见,但在类的外部不可直接访问。

以下是一些关键点来详细解释private的使用和行为:

  1. private成员在类内部可见:private成员只能在类的成员函数内部进行访问。这意味着只有类的其他成员函数可以直接访问private成员。

  2. private成员在类的外部不可见:类的外部无法直接访问类的private成员变量和成员函数。只有通过类的公共成员函数或友元函数才能间接访问。

  3. 封装性:将成员声明为private是一种封装的实现方式,它隐藏了类的内部实现细节,使外部代码无法直接访问和修改类的私有成员。

  4. 数据隐藏:通过将数据成员声明为private,可以确保数据的安全性和完整性。外部代码无法直接修改私有数据成员,只能通过类的公共成员函数来控制数据的访问和修改。

  5. private继承:当一个类以private方式继承另一个类时,基类的publicprotected成员在派生类中变为private成员,只能在派生类的成员函数内部访问。

通过将类的成员声明为private,可以限制对类的内部实现的访问,提供更好的封装性和安全性。这样,类的使用者只能通过类的公共接口进行操作,而无法直接访问和修改类的私有成员。这有助于防止不合法的访问和错误的修改,增加了代码的可维护性和可靠性。

封装是面向对象编程中的一个重要概念,它通过将数据和操作封装在类的内部来保护数据的完整性和安全性。private成员在C++中是实现封装的关键机制之一。


char 关键字

在C++中,char是一种基本数据类型,用于表示字符型数据。与C语言中的char类型相似,C++的char关键字也具有以下特点和用途:

  1. 存储单个字符:char用于存储单个字符,可以包括字母、数字、符号和特殊字符。每个char变量占用1字节的内存空间(8位),用于存储一个字符的ASCII码或其他字符编码。

  2. 字符串的表示:通过将多个char变量排列在一起,可以表示字符串。字符串是由一系列字符组成的字符数组,以空字符\0作为结尾。例如,char str[] = "Hello";定义了一个字符数组str,存储了字符串”Hello”。

  3. 字符类型转换:char类型可以用于表示字符型数据,也可以与整数类型进行相互转换。例如,将一个整数转换为字符类型可以使用强制类型转换,如char ch = static_cast<char>(65);将整数值65转换为字符’A’。

  4. 标识符命名:char可以作为变量或函数的类型标识符,用于声明和定义字符型变量或函数。例如,char ch;声明了一个字符型变量ch

  5. 输入和输出:char类型可以通过输入和输出流对象(如cincout)进行读取和打印。可以使用流提取运算符>>进行字符的输入,使用流插入运算符<<进行字符的输出。

需要注意以下几点:

  • 字符类型使用单引号括起来,例如'A'表示字符’A’。
  • 字符串使用双引号括起来,例如"Hello"表示字符串”Hello”。
  • 在C++中,字符型数据以ASCII码或其他字符编码的方式进行存储和处理。

总结:char关键字在C++中表示字符型数据,用于存储单个字符、表示字符串、进行字符类型转换和标识符命名。它是C++语言中最基本的数据类型之一,具有广泛的应用和作用。


C++ 函数重载 需要用到virtual关键字吗

在 C++ 中,函数重载不需要使用 virtual 关键字。虚函数和函数重载是两个不同的概念。

函数重载是指在同一个作用域内定义多个具有相同名称但参数列表不同的函数。编译器会根据函数的参数列表来确定调用哪个函数。

虚函数是为了实现运行时多态性而引入的概念。通过在基类中声明虚函数,并在派生类中进行重写,可以实现在运行时确定调用哪个函数。虚函数需要使用 virtual 关键字进行声明。

下面是一个示例,展示了函数重载和虚函数的区别:

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

class Base {
public:
    void foo() {
        std::cout << "Base::foo() called" << std::endl;
    }

    virtual void bar() {
        std::cout << "Base::bar() called" << std::endl;
    }
};

class Derived : public Base {
public:
    void foo(int x) {
        std::cout << "Derived::foo(int) called" << std::endl;
    }

    void bar() override {
        std::cout << "Derived::bar() called" << std::endl;
    }
};

int main() {
    Base base;
    Derived derived;

    base.foo();      // 调用 Base::foo()
    derived.foo();   // 调用 Base::foo(),因为 Derived 没有重载 Base::foo()

    base.bar();      // 调用 Base::bar()
    derived.bar();   // 调用 Derived::bar()

    return 0;
}

在上面的示例中,Base 类定义了 foo()bar() 两个函数,Derived 类继承自 Base 类并重写了 bar() 函数,并新增了一个 foo(int) 函数。当调用 foo() 时,根据参数列表的不同,编译器会选择调用对应的函数。而当调用 bar() 时,由于它是一个虚函数并且被派生类重写了,实际执行的函数取决于运行时对象的类型,实现了多态性。

因此,总结起来,函数重载和虚函数是两个不同的概念,在函数重载时不需要使用 virtual 关键字。


多态

多态是面向对象程序设计中的一个重要概念,它允许使用基类的指针或引用调用派生类的成员函数,实现在运行时动态地确定所调用的具体函数。在 C++ 中,多态通过虚函数和指针或引用来实现。

下面是关于 C++ 多态的一些详细解释:

  1. 虚函数:虚函数是在基类中声明的,用关键字 virtual 标识的成员函数。派生类可以重写(覆盖)基类的虚函数,并根据自己的需要重新实现功能。虚函数通过在运行时动态绑定,使得基类的指针或引用可以在运行时调用派生类的实现。

  2. 动态绑定:当基类指针或引用指向派生类对象时,通过虚函数的动态绑定,可以在运行时确定要调用的实际函数。这意味着可以通过基类指针或引用调用派生类的虚函数,而不需要在编译时知道对象的具体类型。

  3. 多态性:多态性是指在运行时根据对象的实际类型来确定所调用的函数。通过多态性,可以实现以统一的方式处理不同类型的对象,并根据对象的实际类型来调用适当的函数。这种灵活性和可扩展性使得代码更加模块化和可重用。

  4. 虚函数表:虚函数表(vtable)是一张存储虚函数地址的表格,每个类(包括基类和派生类)都有自己的虚函数表。当类中有虚函数时,编译器会在对象的内存布局中添加一个指向虚函数表的指针。通过这个指针,可以在运行时查找并调用正确的虚函数。

  5. 纯虚函数和抽象类:纯虚函数是一种没有函数体的虚函数,在基类中声明并赋予 = 0 的值。它要求派生类必须实现该函数。包含纯虚函数的类称为抽象类,抽象类不能被实例化,只能作为接口或基类使用。

多态性在面向对象程序设计中具有重要意义,它提供了代码的灵活性、可扩展性和可维护性。通过使用虚函数和动态绑定,可以编写出更具弹性和可重用性的代码,使得程序更加易于扩展和修改。

多态 详解

在C++中,多态性(Polymorphism)是面向对象编程的一个重要特性,它允许使用基类的指针或引用来调用派生类的成员函数。多态性使得程序可以根据对象的实际类型来确定调用的函数,从而实现代码的灵活性和可扩展性。

C++中的多态性主要通过虚函数(Virtual Function)和运行时类型识别(Run-time Type Identification)来实现。

  1. 虚函数(Virtual Function):在基类中声明虚函数,并在派生类中进行重写。使用虚函数可以使得通过基类指针或引用调用成员函数时,根据实际对象的类型来确定调用的函数。通过在函数声明前面加上virtual关键字来将其声明为虚函数。派生类中重写虚函数时使用override关键字进行标记。

  2. 运行时类型识别(Run-time Type Identification,RTTI):C++提供了dynamic_casttypeid操作符来获取对象的实际类型。dynamic_cast用于将基类指针或引用转换为派生类指针或引用,如果转换失败则返回nullptrtypeid用于获取对象的类型信息,返回type_info对象。

多态性的使用可以带来以下几个优点:

  • 简化代码:通过使用基类指针或引用,可以减少代码的重复性,实现更加简洁和可维护的代码结构。
  • 扩展性:通过派生类的添加,可以在不修改已有代码的情况下扩展程序的功能。
  • 代码重用:通过将多个派生类对象归一化为基类指针或引用,可以实现代码的复用性。

需要注意的是,多态性只适用于通过指针或引用访问对象的成员函数,而不适用于通过对象直接访问成员函数。

总结:C++中的多态性是一种重要的面向对象编程特性,通过虚函数和运行时类型识别实现。它允许使用基类指针或引用来调用派生类的成员函数,根据对象的实际类型确定调用的函数,提供了代码的灵活性和可扩展性。多态性可以简化代码、实现代码的复用和扩展,并为面向对象编程带来更强大的特性。


多态 示例

下面是一个简单的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
#include <iostream>

// 基类
class Animal {
public:
    virtual void sound() {
        std::cout << "动物发出声音" << std::endl;
    }
};

// 派生类1
class Dog : public Animal {
public:
    void sound() override {
        std::cout << "狗在汪汪叫" << std::endl;
    }
};

// 派生类2
class Cat : public Animal {
public:
    void sound() override {
        std::cout << "猫在喵喵叫" << std::endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->sound();  // 输出:狗在汪汪叫
    animal2->sound();  // 输出:猫在喵喵叫

    delete animal1;
    delete animal2;

    return 0;
}

在上述示例中,我们定义了一个基类Animal和两个派生类DogCat。基类中有一个虚函数sound(),并在派生类中进行了重写。

main()函数中,我们创建了基类指针animal1animal2,分别指向DogCat的对象。通过基类指针调用虚函数sound(),实际执行的是派生类中相应的函数。

这种多态的特性使得我们可以通过基类指针或引用来访问派生类对象的成员函数,根据实际对象的类型来确定调用的函数。这样可以实现代码的灵活性和扩展性,使得程序可以适应不同类型的对象。

运行上述示例,输出结果将是:

1
2
狗在汪汪叫
猫在喵喵叫

这表明基类指针animal1animal2在调用虚函数sound()时,分别执行了派生类DogCat中重写的函数。

这就是C++中多态的一个简单示例,通过基类和派生类之间的虚函数实现了运行时的多态性。


std::map

C++11 中的 std::map 是一个关联容器,它提供了一种键值对的映射关系。std::map 使用红黑树数据结构实现,保证了插入、查找和删除操作的平均时间复杂度为 O(log n)。

std::map 的特点如下:

  1. 唯一键值对std::map 中的键值对是唯一的,即每个键只能对应一个值。如果插入重复的键,则新的值会覆盖旧的值。

  2. 排序std::map 中的元素默认按照键的升序进行排序,可以自定义比较函数来指定排序规则。

  3. 动态扩展std::map 是动态扩展的,可以根据需要插入、删除元素,并根据键的值自动调整数据结构。

  4. 快速查找:通过键查找值的操作在平均情况下具有较快的速度,时间复杂度为 O(log n)。

下面是一些常用的 std::map 操作:

  • 插入元素:使用 insert() 函数将一个键值对插入到 std::map 中。
1
2
3
std::map<int, std::string> myMap;
myMap.insert(std::make_pair(1, "one"));
myMap.insert(std::pair<int, std::string>(2, "two"));
  • 访问元素:通过键访问值,使用 operator[]at() 函数。
1
2
std::cout << myMap[1] << std::endl;  // 输出 "one"
std::cout << myMap.at(2) << std::endl;  // 输出 "two"
  • 查找元素:使用 find() 函数查找指定的键。
1
2
3
4
5
6
auto it = myMap.find(1);
if (it != myMap.end()) {
    std::cout << "Found element: " << it->second << std::endl;
} else {
    std::cout << "Element not found" << std::endl;
}
  • 删除元素:使用 erase() 函数删除指定的键。
1
myMap.erase(1);
  • 遍历元素:可以使用迭代器来遍历 std::map 中的所有键值对。
1
2
3
for (auto it = myMap.begin(); it != myMap.end(); ++it) {
    std::cout << it->first << " : " << it->second << std::endl;
}

上述示例代码只是 std::map 的一些常见操作,还有许多其他的函数和特性可以进一步探索和应用。


std::shared_ptr

std::shared_ptr 是 C++11 中引入的一种共享式智能指针,用于管理动态分配的对象。它具有以下特点和用法:

  1. 共享所有权:std::shared_ptr 允许多个智能指针共享同一个对象的所有权。当最后一个 std::shared_ptr 被销毁时,它会自动释放所管理的对象。

  2. 引用计数:std::shared_ptr 内部维护一个引用计数器,用于记录当前有多少个 std::shared_ptr 共享同一个对象。每次创建或拷贝一个 std::shared_ptr,计数器加一;当销毁或重置一个 std::shared_ptr,计数器减一。当计数器变为零时,即没有 std::shared_ptr 指向对象时,对象会被销毁。

  3. 安全地使用动态分配的对象:std::shared_ptr 提供了自动内存管理,可以避免内存泄漏和悬挂指针等问题。它确保了在不需要对象时正确释放资源。

  4. 自定义删除器:可以使用自定义的删除器来释放资源。删除器是一个可调用对象,用于替代默认的 delete 运算符。这允许在对象销毁时执行特定的清理操作。

以下是一些 std::shared_ptr 的用法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建一个 std::shared_ptr 并分配内存
std::shared_ptr<int> ptr(new int(42));

// 使用智能指针操作所管理的对象
*ptr = 10;
std::cout << *ptr << std::endl;

// 创建多个 std::shared_ptr 共享同一个对象
std::shared_ptr<int> ptr2 = ptr;

// 获取共享指针的引用计数
std::cout << "Reference count: " << ptr.use_count() << std::endl;

// 重置 std::shared_ptr,引用计数减一
ptr.reset();

// 获取共享指针的引用计数
std::cout << "Reference count: " << ptr2.use_count() << std::endl;

// 使用 std::make_shared 创建 std::shared_ptr
auto ptr3 = std::make_shared<int>(100);

// 使用自定义删除器释放资源
std::shared_ptr<FILE> file(fopen("file.txt", "r"), fclose);

std::shared_ptr 提供了方便且安全的共享资源管理方式。它适用于多个智能指针需要共享同一个对象所有权的场景,如多个函数间共享资源、构建数据结构等。

需要注意的是,std::shared_ptr 的引用计数机制可能会带来额外的开销,并且可能导致循环引用的问题。在存在循环引用的情况下,需要使用 std::weak_ptr 来打破循环引用关系。


std::unique_ptr

std::unique_ptr 是 C++11 中引入的一种独占式智能指针,用于管理动态分配的对象。它具有以下特点和用法:

  1. 独占所有权:std::unique_ptr 用于管理对象的独占所有权,即同一时间只能有一个 std::unique_ptr 指向对象。当 std::unique_ptr 被销毁时,它所管理的对象也会被自动释放。

  2. 禁止复制:std::unique_ptr 是不可复制的,这意味着不能通过复制构造函数或赋值运算符将其值传递给其他 std::unique_ptr 对象。这样做是为了确保只有一个 std::unique_ptr 拥有资源的所有权。

  3. 支持移动语义:可以使用 std::move 函数将所有权从一个 std::unique_ptr 转移给另一个 std::unique_ptr。移动语义避免了资源的拷贝开销,并将所有权转移给新的 std::unique_ptr

  4. 自动释放资源:当 std::unique_ptr 超出其作用域时,它会自动调用 delete 运算符来释放所管理的对象。这确保了资源的正确释放,避免了内存泄漏。

以下是一些 std::unique_ptr 的用法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 创建一个 std::unique_ptr 并分配内存
std::unique_ptr<int> ptr(new int(42));

// 使用智能指针操作所管理的对象
*ptr = 10;
std::cout << *ptr << std::endl;

// 转移所有权给另一个 std::unique_ptr
std::unique_ptr<int> ptr2 = std::move(ptr);

// 检查指针是否为空
if (ptr2) {
    // 指针不为空
} else {
    // 指针为空
}

// 释放指针所管理的对象(可以手动释放,但不推荐)
ptr2.reset();

// 使用 std::make_unique 创建 std::unique_ptr
auto ptr3 = std::make_unique<int>(100);

std::unique_ptr 在管理动态内存时提供了更高的安全性和便利性,可以避免内存泄漏和悬挂指针等问题。它是现代 C++ 中推荐使用的智能指针之一。

需要注意的是,由于 std::unique_ptr 是独占所有权的,它不能与 C 风格的指针混用,并且需要避免多个 std::unique_ptr 指向同一个对象的情况,以避免悬挂指针的问题。


std::unique_ptr 初始化

在C++中,可以使用std::unique_ptr进行动态内存管理。std::unique_ptr是一个智能指针,用于管理动态分配的对象,并在其生命周期结束时自动释放内存。

下面是几种常见的初始化std::unique_ptr的方式:

  1. 使用new关键字初始化:
    1
    
    std::unique_ptr<int> ptr(new int);  // 初始化为指向动态分配的int对象的unique_ptr
    
  2. 使用std::make_unique函数初始化(C++14及以上版本):
    1
    
    std::unique_ptr<int> ptr = std::make_unique<int>();  // 初始化为指向动态分配的int对象的unique_ptr
    
  3. 使用自定义删除器(deleter)初始化: ```cpp // 使用lambda表达式作为自定义删除器 std::unique_ptr<int, void()(int)> ptr(new int, { delete p; });

// 使用函数指针作为自定义删除器 void deleter(int* p) { delete p; } std::unique_ptr<int, void()(int)> ptr(new int, deleter);

1
2
3
4
5

4. 初始化为`nullptr`或空指针:
```cpp
std::unique_ptr<int> ptr(nullptr);  // 初始化为空指针的unique_ptr
std::unique_ptr<int> ptr;  // 默认初始化为空指针的unique_ptr

需要注意的是,std::unique_ptr具有独占权(exclusive ownership),即同一时间只能有一个std::unique_ptr指向同一块内存。它不允许进行复制操作,但可以进行移动操作。

另外,推荐使用std::make_unique函数进行初始化,因为它更加安全和方便,可以避免手动管理内存分配和释放的问题。

以上是一些常见的std::unique_ptr初始化方式,可以根据具体的需求选择适合的初始化方法。


C++11 智能指针

C++11 引入了智能指针(Smart Pointer)的概念,这些指针用于管理动态分配的内存资源,以减少内存泄漏和悬挂指针等问题。C++11 提供了三种类型的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

  1. std::unique_ptr
    • std::unique_ptr 用于管理独占所有权的指针,即只有一个 std::unique_ptr 可以拥有指针所指向的对象。
    • 使用 std::unique_ptr 可以确保资源在不再需要时被正确释放,因为它在销毁时会自动调用 delete 运算符来释放内存。
    • std::unique_ptr 不能被复制,但可以通过 std::move 来转移所有权。
    • 示例代码:
      1
      
      std::unique_ptr<int> ptr(new int(10));
      
  2. std::shared_ptr
    • std::shared_ptr 用于管理共享所有权的指针,即多个 std::shared_ptr 可以共同拥有指针所指向的对象。
    • 它使用引用计数来跟踪对象的使用情况,并在引用计数变为零时自动释放内存。
    • std::shared_ptr 可以通过 std::make_shared 函数创建,也可以通过复制或移动构造函数进行初始化。
    • 示例代码:
      1
      2
      
      std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
      std::shared_ptr<int> ptr2 = ptr1;
      
  3. std::weak_ptr
    • std::weak_ptr 用于解决 std::shared_ptr 的循环引用问题。
    • 它允许共享对象的访问,但不会增加引用计数。因此,即使存在 std::weak_ptr,对象也可能被释放。
    • 通过调用 std::weak_ptrlock 函数,可以获取一个有效的 std::shared_ptr 来访问对象。
    • 示例代码:
      1
      2
      3
      
      std::shared_ptr<int> sharedPtr = std::make_shared<int>(10);
      std::weak_ptr<int> weakPtr = sharedPtr;
      std::shared_ptr<int> sharedPtr2 = weakPtr.lock();
      

智能指针的使用可以避免手动管理动态内存的复杂性,减少内存泄漏和悬挂指针的风险,并提高代码的安全性和可维护性。

注意:尽管智能指针可以帮助管理内存资源,但仍需要谨慎使用,并注意避免循环引用等问题,以避免内存泄漏。


virtual 关键字

在C++中,virtual 是一个关键字,用于声明虚函数(virtual function)。虚函数是用于实现多态性(polymorphism)的一种机制,在面向对象的编程中非常重要。

当一个类的成员函数被声明为虚函数时,它可以在派生类中被重写(override),并且在运行时根据对象的实际类型来调用相应的函数。这意味着,通过基类的指针或引用调用虚函数时,实际上会调用派生类中的函数,而不是基类中的函数。

虚函数的声明方式是在函数的声明前加上 virtual 关键字。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
    virtual void foo() {
        // 函数体
    }
};

class Derived : public Base {
public:
    void foo() override {
        // 函数体
    }
};

在上面的示例中,Base 类中的 foo() 函数被声明为虚函数,而 Derived 类中的 foo() 函数被重写。通过基类的指针或引用调用 foo() 函数时,实际执行的是派生类 Derived 中的函数。

虚函数的使用有以下几个要点:

  1. 虚函数只能在类的成员函数中声明,并且只能在类的层次结构中使用。
  2. 虚函数可以被继承,派生类可以重写基类中的虚函数。
  3. 虚函数可以通过基类的指针或引用进行动态绑定,实现多态性。
  4. 虚函数可以被纯虚函数(pure virtual function)和抽象类(abstract class)使用。

需要注意的是,虚函数带来了一定的运行时开销,因为需要进行动态绑定。因此,应当合理使用虚函数,避免过度使用导致性能下降。


pragma

#pragma是一个预处理指令,用于向编译器发出特定的指示或控制编译过程中的行为。#pragma指令是与编译器相关的,因此其具体行为和支持的功能取决于使用的编译器。

在C++中,#pragma指令可以用于各种目的,如优化控制、警告控制、库导入等。以下是一些常见的#pragma指令用法:

  1. 警告控制:可以使用#pragma warning指令来控制编译器的警告行为。
1
2
3
#pragma warning(disable: 1234)  // 禁用警告编号为1234的警告
#pragma warning(push)          // 将当前警告状态压入堆栈
#pragma warning(pop)           // 恢复之前的警告状态
  1. 优化控制:可以使用#pragma optimize指令来控制编译器的优化级别。
1
2
#pragma optimize("g", off)    // 关闭所有优化
#pragma optimize("s", on)     // 启用小规模优化
  1. 区域控制:可以使用#pragma region#pragma endregion指令来定义和折叠代码区域。
1
2
3
#pragma region MyCode
// 这里是一段代码
#pragma endregion
  1. 其他用途:不同的编译器可能支持其他特定的#pragma指令,用于特定的功能或行为。

需要注意的是,#pragma指令的具体行为和支持的选项因编译器而异。为了保持代码的可移植性,应当避免过度依赖于特定的#pragma指令。

总之,#pragma是C++编译器提供的一种扩展机制,可以用于控制编译过程中的行为和特定功能的使用。使用时应查阅所使用编译器的文档,了解支持的指令和选项。


线程 分类

在C++中,线程可以根据不同的特性和使用方式进行分类。以下是几种常见的线程分类:

  1. 操作系统线程: 操作系统线程是由操作系统内核创建和管理的线程,也称为内核线程。它们是操作系统的一部分,提供了底层的线程调度和管理。在C++中,使用诸如std::thread等标准库提供的线程类,可以创建和操作操作系统线程。

  2. 用户线程: 用户线程是在用户空间中创建和管理的线程,不依赖于操作系统内核的线程管理机制。在C++中,可以使用第三方库(如Boost.Thread)或操作系统提供的用户线程接口(如pthread)来创建和操作用户线程。用户线程的调度和管理由用户程序自己负责。

  3. 主线程: 主线程是程序启动时由操作系统创建的第一个线程。它是程序的入口点,用于执行程序的主要逻辑。在C++中,主线程通常是main()函数所在的线程。主线程可以创建其他线程,并负责协调它们的执行。

  4. 子线程: 子线程是由主线程或其他线程创建的额外线程。它们通常用于执行一些耗时的任务或并发处理。在C++中,可以使用线程类(如std::thread)来创建子线程。子线程可以并行执行,独立于主线程或其他子线程。

  5. 后台线程: 后台线程是在程序中运行的线程,其执行不会阻止程序的退出。当所有的前台线程(如主线程)退出时,后台线程会自动终止。在C++中,可以使用线程类的成员函数std::thread::detach()将线程设置为后台线程。

  6. 前台线程: 前台线程是程序中运行的线程,它们的执行会阻止程序的退出。当所有的前台线程结束执行时,程序才会退出。在C++中,主线程通常是前台线程。其他前台线程可以通过调用线程类的成员函数std::thread::join()等待其它线程的完成。

这些是一些常见的C++线程分类。根据应用程序的需求,您可以选择适当的线程类型和管理机制来实现并发和多线程编程。

回调函数 存在的意义

回调函数在 C++ 中具有重要的意义,它为编程提供了灵活性和可扩展性。以下是一些回调函数存在的意义:

  1. 事件驱动编程:回调函数是事件驱动编程的核心概念。通过将回调函数注册到特定的事件上,当事件发生时,回调函数会被自动调用。这种机制使得程序能够对各种事件作出响应,例如用户交互、消息传递、I/O 操作完成等。回调函数使得程序能够在特定事件发生时执行相应的逻辑,实现异步和非阻塞的处理方式。

  2. 解耦合和模块化:通过使用回调函数,可以将程序的不同模块解耦合。一个模块可以定义一个回调函数,并将其传递给另一个模块作为参数。这样,两个模块之间的通信可以通过回调函数进行,而不需要直接依赖于具体的实现细节。这种解耦合和模块化的设计使得代码更加清晰、可维护和可重用。

  3. 定制化和扩展性:回调函数使得程序可以根据需要进行定制化和扩展。通过定义不同的回调函数实现,可以在运行时动态地改变程序的行为。这种灵活性使得程序能够适应不同的需求和场景,增强了程序的可扩展性。

  4. 高阶函数和函数式编程:回调函数是高阶函数和函数式编程的基础。高阶函数指的是可以接受其他函数作为参数或返回函数作为结果的函数。通过使用回调函数,可以实现函数的参数化和抽象,使得代码更加简洁和可读。

总的来说,回调函数在 C++ 中具有重要的意义,它们支持事件驱动编程、解耦合和模块化、定制化和扩展性,并为函数式编程提供了基础。回调函数使得程序更加灵活、可扩展和可维护,提高了代码的可读性和可重用性。

线程函数 与 线程

在 C++ 中,线程函数是在线程中执行的函数。线程函数定义了线程的行为和逻辑。以下是有关 C++ 线程函数和线程的详细解释:

  1. 线程函数的定义: 线程函数是一个可调用的实体,可以是普通函数、函数对象或 Lambda 表达式。线程函数的签名(参数列表和返回类型)必须与线程创建时所需的函数类型匹配。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    // 线程函数作为普通函数
    void threadFunction() {
        // 线程逻辑
    }
    
    // 线程函数作为函数对象
    struct ThreadFunctor {
        void operator()() {
            // 线程逻辑
        }
    };
    
    // 线程函数作为 Lambda 表达式
    auto lambda = []() {
        // 线程逻辑
    };
    

    在上述示例中,threadFunctionThreadFunctoroperator() 和 Lambda 表达式都可以作为线程函数。

  2. 启动线程: 可以使用 std::thread 类创建线程并启动线程。例如:

    1
    
    std::thread t(threadFunction);  // 以线程函数启动线程
    

    在上面的示例中,使用线程函数 threadFunction 创建了一个新的线程。

  3. 线程的执行和等待: 一旦线程被创建,它会在调度器中获得执行。可以使用 std::thread 类的成员函数来等待线程的结束。

    • std::thread::join():等待线程执行完成。
    • std::thread::detach():分离线程,允许线程在后台运行。
    1
    2
    3
    
    std::thread t(threadFunction);  // 启动线程
    // ...
    t.join();  // 等待线程执行完成
    

    在上述示例中,t.join() 会等待线程执行完成,确保主线程在子线程结束之前不会退出。

  4. 传递参数给线程函数: 可以通过在创建线程时传递参数来向线程函数传递参数。参数可以是值、引用或移动语义。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    void threadFunction(int value, const std::string& str) {
        // 使用参数执行线程逻辑
    }
    
    int main() {
        int value = 42;
        std::string str = "Hello";
    
        std::thread t(threadFunction, value, std::cref(str));  // 传递参数给线程函数
    
        // ...
    
        t.join();  // 等待线程执行完成
    
        return 0;
    }
    

    在上面的示例中,threadFunction 的参数通过 std::thread 构造函数传递给线程函数。

线程函数是多线程编程中的重要概念,它定义了线程的行为和任务。合理地设计和编写线程函数可以

帮助实现并发性和并行性,并处理共享数据、同步和线程间通信等问题。在 C++ 中,可以使用 <thread> 头文件提供的类和函数来创建和管理线程,实现多线程应用程序。

多线程

C++ 中的多线程编程允许程序在同一时间执行多个独立的执行流,从而提高并发性和性能。C++ 11 引入了 <thread> 头文件,提供了一组用于创建和管理线程的类和函数。以下是关于 C++ 多线程的详细解释:

  1. 创建线程: 可以使用 std::thread 类来创建一个新的线程。创建线程的一种常见方式是通过函数指针或函数对象创建线程。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    #include <iostream>
    #include <thread>
    
    // 线程函数
    void threadFunction() {
        std::cout << "Hello from thread!" << std::endl;
    }
    
    int main() {
        // 创建新线程
        std::thread t(threadFunction);
    
        // 主线程继续执行其他操作
        std::cout << "Hello from main thread!" << std::endl;
    
        // 等待线程结束
        t.join();
    
        return 0;
    }
    

    在上面的例子中,std::thread t(threadFunction) 创建了一个新的线程,并在该线程中执行 threadFunction 函数。t.join() 等待线程结束。

  2. 传递参数给线程: 可以通过将参数传递给线程的构造函数来向线程传递参数。参数可以是值、引用或移动语义。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    #include <iostream>
    #include <thread>
    
    void threadFunction(int value, const std::string& str) {
        std::cout << "Value: " << value << std::endl;
        std::cout << "String: " << str << std::endl;
    }
    
    int main() {
        int value = 42;
        std::string str = "Hello";
    
        std::thread t(threadFunction, value, std::cref(str));
    
        t.join();
    
        return 0;
    }
    

    在上面的例子中,threadFunction 函数接受一个整数和一个字符串作为参数。通过在 std::thread 构造函数中传递这些参数来向线程传递参数。注意使用 std::cref 来传递 std::string 的引用。

  3. 线程同步: 在多线程编程中,需要注意线程之间的同步,以避免数据竞争和不确定的行为。C++ 提供了多种线程同步机制,如互斥量 (std::mutex)、条件变量 (std::condition_variable)、原子操作 (std::atomic) 等,用于保护共享数据的访问和实现线程间的通信。

    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>
    #include <thread>
    #include <mutex>
    
    std::mutex mtx;
    int sharedData = 0;
    
    void threadFunction() {
        std::lock_guard<std::mutex> lock(mtx);
    
        sharedData++;
    }
    
    int main() {
        std::thread t1(threadFunction);
        std::thread t2(threadFunction);
    
        t1.join();
        t2.join();
    
        std::cout << "Shared Data: " << sharedData << std::endl;
    
        return 0;
    }
    

    在上面的例子中,std::mutex 用于保护共享数据 sharedData 的访问。通过使用 std::lock_guard,可以确保在离开作用域时自动释放互斥量。

以上是 C++ 中多线程编程的基础知识。多线程编程需要谨慎处理共享数据、同步操作和避免竞态条件等问题。在实际应用中,还可以探索更多的线程管理、任务分发和线程池等概念和技术,以实现更复杂的多线程应用程序。

队列

在C++中,<queue>是一个标准库头文件,提供了队列(Queue)容器的实现。队列是一种遵循先进先出(First-In-First-Out,FIFO)原则的容器,允许在队尾插入元素,在队头删除元素。下面是关于<queue>的详细解释:

  1. 队列容器的创建:
    1
    2
    3
    
    #include <queue>
    
    std::queue<T> queueObject;
    

    这里的 T 是队列中存储的元素类型,可以是任意类型。

  2. 队列容器的基本操作:
    • push(value): 将元素插入队列的末尾。
    • pop(): 删除队列头部的元素,不返回其值。
    • front(): 返回队列头部元素的引用,不对队列进行修改。
    • back(): 返回队列末尾元素的引用,不对队列进行修改。
    • empty(): 判断队列是否为空,返回布尔值。
    • size(): 返回队列中元素的个数。

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    std::queue<int> queueObject;
    queueObject.push(10);  // 插入元素10
    queueObject.push(20);  // 插入元素20
    
    int frontElement = queueObject.front();  // 获取队列头部元素(10)
    int backElement = queueObject.back();  // 获取队列末尾元素(20)
    queueObject.pop();  // 删除队列头部元素(10)
    
    bool isEmpty = queueObject.empty();  // 判断队列是否为空(false)
    int queueSize = queueObject.size();  // 获取队列中元素的个数(1)
    
  3. 队列容器的特点:
    • 队列是一种动态数据结构,它会自动调整大小以适应存储的元素数量。
    • 队列允许在队尾插入元素,在队头删除元素,保持了元素插入和删除的顺序。
    • 队列的元素按照先进先出(FIFO)的原则进行访问。
  4. 注意事项:
    • <queue>头文件只提供了队列的基本操作,不支持直接迭代访问元素。
    • 如果需要按照顺序访问队列的所有元素,可以先将队列中的元素弹出到另一个容器,然后逐个访问。

队列是一个常用的数据结构,适用于需要先进先出操作的场景。<queue>头文件提供了方便的接口和操作,使得队列的使用变得简单和高效。

字 字节

在 Linux 系统中,字和字节是计算机存储和处理数据的基本单位。下面是对字和字节的详细解释:

  1. 字节(Byte):
    • 字节是计算机中存储和传输数据的最小单位。
    • 一个字节由8个二进制位(bit)组成,每个二进制位可以表示0或1。
    • 字节通常用于存储和表示字符、整数和其他数据类型。
  2. 字(Word):
    • 字是计算机中用于处理数据的基本单位,其大小取决于特定计算机架构和操作系统。
    • 在不同的系统中,字的大小可以是8位(1字节)、16位(2字节)、32位(4字节)或64位(8字节)等。
    • 字一般用于处理整数、浮点数、指针等数据类型,以及进行计算和操作。

在大多数现代计算机架构中,一个字节由8个二进制位组成,并且一个字通常等于一个或多个字节。例如,常见的 x86 架构中,一个字节等于8位,一个字通常等于4字节(32位系统)或8字节(64位系统)。

需要注意的是,字和字节的大小可以根据不同的计算机架构和操作系统而异。因此,在编程和系统开发中,需要明确指定字节和字的大小以确保正确处理和操作数据。

C++ 文件读取到内存,为什么都是以char数据类型存储

在C++中,文件读取到内存通常使用 char 类型存储数据,是因为 char 类型是最基本的字节类型,在内存中以字节为单位存储数据。文件可以被视为字节流,而 char 类型正好可以表示一个字节的数据。

使用 char 类型存储文件数据有以下几个原因:

  1. 文件是由字节组成的:文件内容是以字节为单位存储的,而 char 类型正好可以表示一个字节的数据,因此使用 char 类型存储文件数据最为直观和自然。

  2. 字节操作灵活:使用 char 类型存储文件数据可以方便地进行字节级的操作和处理,如读取、写入、拷贝、比较等。

  3. 兼容性和可移植性:char 类型在不同平台和系统中具有相同的字节大小(通常为 1 字节),因此在文件读取和内存操作时具有良好的兼容性和可移植性。

尽管文件可以包含不同的数据类型,例如整数、浮点数、字符串等,但在读取文件到内存时,通常先将文件内容视为字节流,再根据具体的数据格式和解析需求对字节进行解析和转换。这样可以灵活地处理各种不同类型的文件数据。

当需要读取其他数据类型时,可以使用类型转换和解析操作将 char 类型的数据转换为目标数据类型。例如,可以使用 reinterpret_castmemcpy 等操作将 char 数组转换为其他类型的数据。

C++ 内存共享

在C++中,内存共享是一种在多个进程之间共享内存数据的机制。它允许不同的进程访问相同的内存区域,以实现进程间的数据共享和通信。

C++中实现内存共享通常可以使用以下两种机制:

  1. 共享内存对象:可以使用操作系统提供的共享内存机制,如shmgetshmat等函数,或者使用第三方库,如Boost.Interprocess库。共享内存对象允许多个进程将同一块内存映射到它们的地址空间中,并通过该内存进行数据交换。进程可以直接读写共享内存中的数据,而无需复制数据。

  2. 内存映射文件:可以通过将文件映射到内存中来实现进程间的内存共享。使用mmap函数可以将文件映射到进程的地址空间,并让多个进程共享同一块内存区域。进程可以通过对内存的读写来实现数据共享和通信。

无论使用哪种机制,内存共享需要考虑同步和互斥的问题,以确保数据的一致性和正确性。常见的同步和互斥机制包括使用信号量、互斥锁、读写锁等。

下面是一个使用共享内存对象进行内存共享的简单示例:

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>
#include <sys/ipc.h>
#include <sys/shm.h>

struct SharedData {
    int value;
    char message[256];
};

int main() {
    key_t key = ftok("/tmp/memfile", 1);
    int shmId = shmget(key, sizeof(SharedData), IPC_CREAT | 0666);
    SharedData* sharedData = static_cast<SharedData*>(shmat(shmId, nullptr, 0));
    
    // 写入共享内存数据
    sharedData->value = 42;
    std::strcpy(sharedData->message, "Hello, shared memory!");
    
    // 从共享内存中读取数据
    std::cout << "Value: " << sharedData->value << std::endl;
    std::cout << "Message: " << sharedData->message << std::endl;
    
    shmdt(sharedData);
    shmctl(shmId, IPC_RMID, nullptr);
    
    return 0;
}

上述示例使用了shmget函数创建共享内存对象,并使用shmat函数将共享内存映射到进程的地址空间中。通过共享内存指针sharedData,可以直接读写共享内存中的数据。

需要注意的是,共享内存的创建、附加、分离和销毁都需要使用相应的函数进行操作,并且需要正确地处理错误和同步机制,以确保多个进程之间的数据一致性和安全性。

C++项目开发结构

在C++项目开发中,第三方源代码通常会被放置在项目的特定目录中,以便管理和使用。常见的做法是在项目根目录下创建一个名为 “third_party” 或 “external” 的目录,用于存放第三方源代码。

项目结构示例:

1
2
3
4
5
6
7
8
9
10
- project/
  - src/       // 项目源代码目录
  - include/   // 项目头文件目录
  - lib/       // 项目库文件目录
  - bin/       // 项目可执行文件目录
  - test/      // 单元测试目录
  - third_party/   // 第三方源代码目录
    - library1/    // 第三方库1
    - library2/    // 第三方库2
    - ...

在第三方源代码目录中,可以进一步按照每个第三方库的名称创建子目录,并将该库的源代码、头文件、编译配置等放置在相应的目录下。这样可以将第三方代码与项目的自有代码进行分离,使项目结构更加清晰,并方便进行管理和维护。

当你引用第三方库时,可以根据需要进行编译、链接或包含相应的源代码、头文件和库文件。具体的操作步骤和配置可能因项目构建系统的不同而有所差异。一般来说,你需要在项目的构建脚本或配置文件中指定第三方库的路径和相关编译选项,以确保能够正确地使用第三方库。

需要注意的是,当使用第三方库时,你可能需要遵循该库的许可证要求,并遵守相应的开源许可协议。在使用和分发第三方代码之前,请务必仔细阅读和理解相应的许可条款。

C++ 插件

C++ 插件是指使用 C++ 编程语言编写的可扩展模块,它们可以被动态加载到应用程序中,以增加或修改应用程序的功能。插件通常是独立的动态链接库(DLL)或共享对象(SO)文件,它们与应用程序分离,可以在运行时进行加载和卸载。

C++ 插件在软件开发中具有许多应用,例如:

  1. 扩展功能:通过加载插件,应用程序可以动态添加新功能,而无需重新编译和部署整个应用程序。这使得应用程序更加灵活和可扩展。

  2. 定制化和配置:插件可以用于提供不同的定制化选项或配置方案。用户可以根据自己的需求选择和加载适当的插件,以满足其特定的要求。

  3. 外部模块集成:插件可以用于集成第三方库、组件或模块到应用程序中。这使得应用程序能够利用外部库的功能,提高开发效率和功能丰富性。

编写 C++ 插件涉及以下步骤:

  1. 定义插件接口:确定插件的功能和提供的接口,包括函数、类和数据结构等。

  2. 实现插件功能:使用 C++ 编程语言编写插件的具体实现。这可以包括编写插件的功能代码、算法、数据处理等。

  3. 封装为动态链接库:将插件代码编译为动态链接库,例如在 Linux 上生成共享对象(SO)文件,在 Windows 上生成动态链接库(DLL)文件。

  4. 加载和使用插件:应用程序通过动态加载机制加载插件,并使用插件提供的功能。这可以通过使用操作系统提供的动态加载库、插件管理器或框架来实现。

需要注意的是,C++ 插件的开发和使用可以因操作系统、编程语言和框架而异。具体的插件开发流程和使用方式可能会因你所使用的技术栈和环境而有所不同。

函数对象

  • 可以重载类中的函数调用运算符,以便用类的对象代替函数指针。这些对象称为函数对象,或者简称仿函数
  • 使用函数对象而不是使用简单函数的好处是:函数对象可以在调用之间保持状态。

  • 要使任何类称为函数对象,只需要重载函数调用运算符。

函数指针

  • C++中的函数被称为一级函数,因为函数可以像普通变量一样使用,例如将它们作为参数传递给其他函数,从其他函数返回它们,将它们赋值给变量。
  • 在这个上下文中经常出现的术语叫回调,它表示可以调用的东西,它可以使函数指针或任何行为类似于函数指针的东西,比如重载了operator()的对象,或内联lambda表达式。
  • 重载operator()的类称为函数对象,简称functor

  • 函数指针根据参数类型和兼容函数的返回类型进行类型划分,使用函数指针的一种方法是使用类型别名,类型别名允许将类型名称赋值给具有给定特征的函数族。
  • 举例,下面一行定义了一种名为Matcher的类型,它表示指向任何具有两个int形参并返回bool的函数的指针:
    • using Matcher = bool (*)(int, int);
  • 下面的类型别名定义了一种名为MatchHandler的类型,用于接收一个size_t和两个int型作为参数和没有返回值的函数
    • using MatchHandler = void (*)(size_t, int, int);
  • 既然定义了这些类型,可以编写一个接收两个回调(Matcher, MatchHandler)作为参数的函数。
  • 接收其他函数作为参数的函数,或返回函数的函数称为高阶函数

关于命令行参数

  • 命令行参数可以分为两类,一类是短选项,一类是长选项。短选项在参数前加一杠-,长选项在参数前连续加两杠--
    • -a,-A,-b 都表示短选项
    • –all,–almost-all,–author 都表示长选项
  • 他们两者后面都可选择性添加额外参数。如:–block-size=SIZE,其中 SIZE 便是额外参数。

C/C++程序的主函数参数

  • C/C++程序的主函数有两个参数:
    • 第一个参数是整型,可以获得包括程序名字的参数个数
    • 第二个参数是字符数组指针或字符指针的指针,可以按顺序获得命令行上各个字符串参数。
  • 其原形是:
    1
    2
    3
    
      int main(int argc, char *argv[]);
      //或者
      int main(int argc, char **argv);
    
  • 如何解析命令行输入的参数,可以使用下面三个glibc库函数来实现
    • int getopt(int argc, char * const argv[],const char *optstring)
    • int getopt_long(int argc, char * const argv[],const char *optstring,const struct option *longopts, int *longindex);
    • int getopt_long_only(int argc, char * const argv[],const char *optstring,const struct option *longopts, int *longindex);
  • 三者的区别:
    • getopt()只支持短格式选项
    • getopt_long()既支持短格式选项,又支持长格式选项
    • getopt_long_only()用法和getopt_long()完全一样,唯一的区别在输入长选项的时候可以不用输--而使用-
  • 一般情况下,使用getopt_long()来完成命令行选项以及参数的获取。

静态库和动态库的区别

  • 载入时间:
    • 静态库的代码在编译的过程中已经载入到可执行文件中,所以最后生成的可执行文件比较大
    • 动态库的代码在可执行程序运行时才载入内存,在编译过程中仅简单的引用,所以最后生成的可执行文件相对较小
  • 最大的区别是:
    • 静态库链接的时候,把库直接加载到程序中,
    • 而动态库链接的时候,它只是保留接口,将动态库和程序代码独立,这样就可以提高代码的可复用度和降低程序的耦合性
  • 代码使用的区别:
    • 静态库在程序编译时,会被链接到目标代码中,程序运行时不再需要该静态库
    • 动态库在程序编译时,并不会被链接到目标代码中,而是在程序运行时才被载入,因此在程序运行的时候还需要动态库存在
  • 注:
    • 无论是静态库,还是动态库,都是由.o文件创建的,因此,我们必须将源程序通过gcc编译成.o文件

查找C/C++的路径

  • C
    • gcc -v -E -x c -
  • C++
    • gcc -v -E -x c++ -

如何确定gcc支持的C++版本

  • 根据gcc的版本号来推断
    • gcc –version: 可以用来查看版本号,例如是8.3.0,查询8.3.0版本的gcc是那一年发布的,从而推断其支持的C++标准
  • man gcc
    • 查询帮助,通过检索-std 查看该参数支持的值

C命令行参数

  • 执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。
  • 命令行参数是使用 main() 函数参数来处理的,其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。

  • 应当指出的是,
    • argv[0] 存储程序的名称,
    • argv[1] 是一个指向第一个命令行参数的指针,
    • *argv[n] 是最后一个参数。
    • 如果没有提供任何参数,argc 将为 1,否则,如果传递了一个参数,argc 将被设置为 2。
  • 多个命令行参数之间用空格分隔,但是如果参数本身带有空格,那么传递参数的时候应把参数放置在双引号 “” 或单引号 ‘’ 内部。

  • Linux 下我们可使用 getopt 和 getopt_long 来对命令行参数进行解析,

POSIX操作系统API

unistd.h

  • unistd.h, C 和 C++ 程序设计语言中提供对 POSIX 操作系统 API 的访问功能的头文件的名称
  • unistd.h, 是 C 和 C++ 程序设计语言中提供对 POSIX 操作系统 API 的访问功能的头文件的名称。该头文件由 POSIX.1 标准(可移植系统接口)提出,故所有遵循该标准的操作系统和编译器均应提供该头文件(如 Unix 的所有官方版本,包括 Mac OS X、Linux 等)。

  • 对于类 Unix 系统,unistd.h 中所定义的接口通常都是大量针对系统调用的封装(英语:wrapper functions),如 fork、pipe 以及各种 I/O 原语(read、write、close 等等)。

C/C++中类的几种成员函数声明后必须要定义吗?

  • 纯虚函数出现在接口类中,并且赋值为0,不要为该函数分配函数地址,从而阻止类的实例化。纯虚函数是没有定义的

  • 一般的成员函数可以只有声明,但前提是在应用中不能调用该函数,否则会因为找不到定义而产生错误。

C/C++中浮点数的格式化

  • 处理原则:
    • 无论何时需要格式化一个数值,都应该先转为一个字符串,这样可以保证每一位数刚好可以占据一个字符
    • 在需要转换为字符串时,请使用库

C/C++ 位域知识小结

  • C/C++中以一定区域内的位(bit)为单位来表示的数据称为位域,位域必须指明具体的数目
  • 位域的作用主要是节省内存资源,使数据结构更紧凑。

  • 一个位域必须存储在同一个字节中,不能跨两个字节,故位域的长度不能大于一个字节的长度
  • 如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      struct BitField {
        unsigned int a:4;  //占用4个二进制位;
        unsigned int  :0;  //空位域,自动置0;
        unsigned int b:4;  //占用4个二进制位,从下一个存储单元开始存放;
        unsigned int c:4;  //占用4个二进制位;
        unsigned int d:5;  //占用5个二进制位,剩余的4个bit不够存储4个bit的数据,从下一个存储单元开始存放;
        unsigned int  :0;  //空位域,自动置0;
        unsigned int e:4;  //占用4个二进制位,从这个存储单元开始存放;
      };
    
  • 取地址操作符&不能应用在位域字段上;

  • 位域字段不能是类的静态成员;

  • 位域字段在内存中的位置是按照从低位向高位的顺序放置的;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      struct BitField {
        unsigned char a:2;  //最低位;
        unsigned char b:3;
        unsigned char c:3;  //最高位;
      };
      union Union {
        struct BitField bf;
        unsigned int n;
      };
      union Union ubf;
      ubf.n = 0;    //初始化;
      ubf.bf.a = 0; //二进制为: 000
      ubf.bf.b = 0; //二进制为: 000
      ubf.bf.c = 1; //二进制为: 001
      printf("ubf.bf.n = %u\n", ubf.n);
    
  • 位域中的位域字段按照从低位向高位顺序方式的顺序来看,那么,a、b、c这三个位域字段在内存中的放置情况是:
    • 最高位是c:001
    • 中间位是b:000
    • 最低位是a:000;
  • 所以,这个位域结构中的8二进制内容就是: 00100000,总共8个位,其十进制格式就是32;实际上打印出来的ubf.n值就是32;
    1
    2
    3
    
      ubf.n = 100; //二进制为: 01100100
    
      printf("ubf.bf.a = %d, ubf.bf.b = %d, ubf.bf.c = %d\n", ubf.bf.a, ubf.bf.b, ubf.bf.c);
    
  • 此时,对于位域ubf.bf来说,其位于字段仍然按照从低位向高位顺序方式的顺序放置,则,
    • 最高位是c:011,
    • 中间位是b:001,
    • 最低位是a:00;
  • 所以,ubf.bf.a = 0; ubf.bf.b = 1; ubf.bf.c = 3;实际上打印出来的结果也的确如此;不够存储下一个位域的4位,故设为空位域,不使用,自动置0;e从第四个字节处开始存放,占用4位;

  • 位域的对齐
    • 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
    • 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
    • 如果相邻的两个位域字段的类型不同,则各个编译器的具体实现有差异,VC6采取不压缩方式,GCC和Dev-C++都采用压缩方式;
    • 整个结构体的总大小为最宽基本类型成员大小的整数倍
    • 如果位域字段之间穿插着非位域字段,则不进行压缩;(不针对所有的编译器)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      struct BFA {
        unsigned char a:2;
        unsigned char b:3;
        unsigned char c:3;
      };
      struct BFB {
        unsigned char a:2;
        unsigned char b:3;
        unsigned char c:3;
        unsigned int  d:4;  //多出来这个位域字段;
      };
      
  • sizeof(BFA)=1, sizeof(BFB)=8;
  • 这也说明了第三点中”相邻两个位于字段类型不相同时,VC6采取不压缩的方式”

  • 当要把某个成员说明成位域时,其类型只能是int,unsigned int与signed int三者之一(说明:int类型通常代表特定机器中整数的自然长度。short类型通常为16位,long类型通常为32位,int类型可以为16位或32位.各编译器可以根据硬件特性自主选择合适的类型长度.见The C Programming Language中文 P32)
  • 尽管使用位域可以节省内存空间,但却增加了处理时间,在为当访问各个位域成员时需要把位域从它所在的字中分解出来或反过来把一值压缩存到位域所在的字位中.

类中的静态变量和静态函数

1.1 详解

  • 示例:
    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>
    
      using namespace std;
    
      class Test
      {
      public:
          Test() : y(1), r(y), d(3){} //对于常量型成员变量和引用型成员变量,必须通过参数化列表的方式进行初始化。
          ~Test(){}
    
          int y;      //普通变量成员
          int &r;     //引用成员变量
          const int d;    //常量成员变量
          static int c;   //静态成员变量
          static const int x = 2.1;   //静态常量整型成员变量
          static const int xx;        //静态常量整型成员变量声明
          static const double z;  //静态常量非整型成员变量声明
          static const float zz = 6.6;    //静态常量非整型成员变量
      };
    
    
      const int Test::xx = 4; //静态常量整型成员变量定义
      const double Test::z = 5.1; ////静态常量非整型成员变量定义
      int Test::c = 2;
    
      int main(void)
      {
          cout << Test::x << endl;    
    
          return 0;
      }
    
  • 这些特殊类型的成员变量主要有:
    • 引用
    • 常量
    • 静态变量
    • 静态整型常量
    • 静态非整型常量
  • 对于引用和常量,成员变量必须通过构造函数的参数列表的方式初始化。上述程序中的r 和 d 变量的初始化
  • 对于静态变量,static成员变量需要在类定义体外进行初始化与定义,因为static数据成员独立该类的任意对象存在,它是类关联的对象,不与类对象关联。例如:上述程序中的C变量的初始化
  • 对于静态整型常量,该类型成员可以直接在类中初始化,也可以在类中声明,在类定义体外进行定义。例如:上述程序中的x和xx变量。
  • 对于静态非整型常量,该类型也是可以在类中声明,在类定义体外进行定义,或者直接在类中定义并初始化。例如:上述程序中的z和zz变量。

1.2 结论

  • static成员的优点
    • static成员的名字是在类的作用域中,因此可以避免与其它类成员或全局对象名字冲突。
    • 可以实施封装,static成员可以是私有的,而全局对象不可以。
    • 阅读程序容易看出static成员与某个类相关联,这种可见性可以清晰地反映程序员的意图。
  • static成员函数特点
    • 因为static成员函数没有this指针,所以静态成员函数不可以访问非静态成员。
    • 非静态成员函数可以访问静态成员。
    • 静态数据成员与类的大小无关,因为静态成员只是作用在类的范围而已。

1.3 static用法总结

  • c语言中:
    • 用于函数内部修饰变量,即函数内的静态变量。这种变量的生存期长于该函数,使得函数具有一定的“状态”。使用静态变量的函数一般是不可重入的,也不是线程安全的,比如strtok(3)。
    • 用在文件级别(函数体之外),修饰变量或函数,表示该变量或函数只在本文件可见,其他文件看不到也访问不到该变量或函数。专业的说法叫“具有internal linkage”(简言之:不暴露给别的translation unit)
  • c++语言中(由于C++引入了类,在保持与C语言兼容的同时,static关键字又有了两种新用法):
    • 用于修饰类的数据成员,即所谓“静态成员”。这种数据成员的生存期大于class的对象(实例/instance)。静态数据成员是每个class有一份,普通数据成员是每个instance 有一份。
    • 用于修饰class的成员函数,即所谓“静态成员函数”。这种成员函数只能访问静态成员和其他静态成员函数,不能访问非静态成员和非静态成员函数。

智能指针与内存管理

RAII与引用计数

  • 引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
  • 在传统 C++ 中,『记得』手动释放资源,总不是最佳实践。因为我们很有可能就忘记了去释放资源而导致泄露。 所以通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 RAII 资源获取即初始化技术
  • 凡事都有例外,我们总会有需要将对象在自由存储上分配的需求,在传统 C++ 里我们只好使用 new 和 delete 去 『记得』对资源进行释放。而 C++11 引入了智能指针的概念,使用了引用计数的想法,让程序员不再需要关心手动释放内存。 这些智能指针包括 std::shared_ptr/std::unique_ptr/std::weak_ptr,使用它们需要包含头文件 <memory>
  • 注意:引用计数不是垃圾回收,引用计数能够尽快收回不再被使用的对象,同时在回收的过程中也不会造成长时间的等待, 更能够清晰明确的表明资源的生命周期。

std::shared_ptr

  • std::shared_ptr 是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。但还不够,因为使用 std::shared_ptr 仍然需要使用 new 来调用,这使得代码出现了某种程度上的不对称。
  • std::make_shared 就能够用来消除显式的使用 new,所以 std::make_shared 会分配创建传入参数中的对象, 并返回这个对象类型的 std::shared_ptr 指针
  • 例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
      #include <iostream>
      #include <memory>
      void foo(std::shared_ptr<int> i) {
          (*i)++;
      }
      int main() {
          // auto pointer = new int(10); // illegal, no direct assignment
          // Constructed a std::shared_ptr
          auto pointer = std::make_shared<int>(10);
          foo(pointer);
          std::cout << *pointer << std::endl; // 11
          // The shared_ptr will be destructed before leaving the scope
          return 0;
      }
    
  • std::shared_ptr 可以通过 get() 方法来获取原始指针,通过 reset() 来减少一个引用计数, 并通过 use_count() 来查看一个对象的引用计数

std::unique_str

  • std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全:
    1
    2
    
      std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入
      std::unique_ptr<int> pointer2 = pointer; // 非法
    
  • make_unique 并不复杂,C++11 没有提供 std::make_unique,可以自行实现:
    1
    2
    3
    4
    
      template<typename T, typename ...Args>
      std::unique_ptr<T> make_unique( Args&& ...args ) {
        return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
      }
    
  • 既然是独占,换句话说就是不可复制。但是,我们可以利用 std::move 将其转移给其他的 unique_ptr
  • 例如:
    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
    
      #include <iostream>
      #include <memory>
    
      struct Foo {
          Foo() { std::cout << "Foo::Foo" << std::endl; }
          ~Foo() { std::cout << "Foo::~Foo" << std::endl; }
          void foo() { std::cout << "Foo::foo" << std::endl; }
      };
    
      void f(const Foo &) {
          std::cout << "f(const Foo&)" << std::endl;
      }
    
      int main() {
          std::unique_ptr<Foo> p1(std::make_unique<Foo>());
          // p1 不空, 输出
          if (p1) p1->foo();
          {
              std::unique_ptr<Foo> p2(std::move(p1));
              // p2 不空, 输出
              f(*p2);
              // p2 不空, 输出
              if(p2) p2->foo();
              // p1 为空, 无输出
              if(p1) p1->foo();
              p1 = std::move(p2);
              // p2 为空, 无输出
              if(p2) p2->foo();
              std::cout << "p2 被销毁" << std::endl;
          }
          // p1 不空, 输出
          if (p1) p1->foo();
          // Foo 的实例会在离开作用域时被销毁
      }
    

std::weak_ptr

  • 如果你仔细思考 std::shared_ptr 就会发现依然存在着资源无法释放的问题。看下面这个例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
      struct A;
      struct B;
    
      struct A {
          std::shared_ptr<B> pointer;
          ~A() {
              std::cout << "A 被销毁" << std::endl;
          }
      };
      struct B {
          std::shared_ptr<A> pointer;
          ~B() {
              std::cout << "B 被销毁" << std::endl;
          }
      };
      int main() {
          auto a = std::make_shared<A>();
          auto b = std::make_shared<B>();
          a->pointer = b;
          b->pointer = a;
      }
    
  • 运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露
  • 解决这个问题的办法就是使用弱引用指针 std::weak_ptrstd::weak_ptr 是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)

  • std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它可以用于检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false,否则返回 true
  • 除此之外,它也可以用于获取指向原始对象的 std::shared_ptr 指针,其 lock() 方法在原始对象未被释放时,返回一个指向原始对象的 std::shared_ptr 指针,进而访问原始对象的资源,否则返回 nullptr

__sync_fetch_and_add系列

  • 作用:提供多线程下变量的加减和逻辑运算的原子操作
  • 因为是内置函数,所以使用的时候不需要包含任何头文件

  • 存在原因:
    • count++ 这种操作不是原子的。一个自加操作,本质上是分成三步的:
      • 从缓存取到寄存器
      • 在寄存器加1
      • 存入缓存
    • 由于时序的因素,多个线程操作同一个全局变量,会出现问题。这也是并发编程的难点。在目前多核条件下,这种困境会越来越彰显出来
    • 最简单的处理办法就是加锁保护。
    • 使用__sync_fetch_and_add,对于多线程对全局变量进行自加,就不用线程锁了
    • __sync_fetch_and_add系列的处理速度是线程锁的6-7倍
  • __sync_fetch_and_add系列一共有是十二个函数,有加/减/与/或/异或/等函数的原子性操作函数
  • __sync_fetch_and_add,顾名思义,先fetch,然后自加,返回的是自加之前的数值。
    • count = 4为例,调用__sync_fetch_and_add(&count, 1)之后,返回值是4,然后,count变成了5
  • 全部函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      type __sync_fetch_and_add (type *ptr, type value);
      type __sync_fetch_and_sub (type *ptr, type value);
      type __sync_fetch_and_or (type *ptr, type value);
      type __sync_fetch_and_and (type *ptr, type value);
      type __sync_fetch_and_xor (type *ptr, type value);
      type __sync_fetch_and_nand (type *ptr, type value);
      type __sync_add_and_fetch (type *ptr, type value);
      type __sync_sub_and_fetch (type *ptr, type value);
      type __sync_or_and_fetch (type *ptr, type value);
      type __sync_and_and_fetch (type *ptr, type value);
      type __sync_xor_and_fetch (type *ptr, type value);
      type __sync_nand_and_fetch (type *ptr, type value);
    
  • 这些都是在C++11以后支持的

  • 由一个概念叫做:无锁化编程,知道Linux支持的哪些操作是具有原子特性的是理解和设计无锁化编程算法的基础
  • 如果是想使用全局变量来做统计操作,而又不得不考虑多线程间的互斥访问的话,最好使用编译器支持的原子操作函数。在满足互斥访问的前提下,编程最简单,效率最高
  • lock-free,无锁编程方式确实能够比传统加锁方式效率高。所以在高并发程序中采用无锁编程的方式可以进一步提高程序效率。但是得对无锁方式有足够熟悉的了解,不然效率反而会更低而且容器出错。

  • 无锁编程与分布式编程
    • 无锁编程主要是使用原子操作替代锁来实现对共享资源的访问保护
    • 在多核系统中,因为多个CPU核在物理上是并行的,可能发生同时写的现象;所以必须保证一个CPU核在对共享内存进行写操作时,其他CPU核不能写这块内存。因此在多核系统中和单核有区别,即时只有一条执行,也需要加锁保护
    • 在无锁编程环境中(Lock-free),主要使用的原子操作为CAS(Compare and Swap)操作。使用这种原子操作替代锁的最大的一个好处是它是非阻塞的
    • 分布式编程设计的主要特征是分布和通信
      • 采用分布式程序设计方法设计程序时,一个程序由若干个可独立执行的程序模块组成。这些程序模块分布于一个分布式计算机系统的几台计算机上同时执行。

      • 分布式程序设计语言与常用的各种程序设计语言的主要区别:在于它具有程序分布和通信的简述。因此,分布式程序设计语言,往往可以由一种程序设计语言增加分布和通信的简述而构成。 + 分布在各台计算机上的程序模块是相互关联的,它们在执行中需要交换数据,即通信。只有通过通信,各程序模块才能协调地完成一个共同的计算任务。
      • 采用分布式程序设计方法解决计算问题,必须提供用以进行分布式程序设计的语言和设计相应的分布式算法。
      • 分布式算法和适用于多处理器系统的并行算法,都具有并行执行的特点,但它们是有区别的。
        • 设计分布算法时,必须保证实现算法的各程序模块间不会有公共变量,它们只能通过通信来交换数据。此外,设计分布式算法时,往往需要考虑坚定性,即**当系统中几台计算机失效时,算法仍然是v有效的。

强制转换运算符(强制类型转换)

  • 强制转换运算符是一种特殊的运算符,它把一种数据类型转换为另一种数据类型。
  • 强制转换运算符是一元运算符,它的优先级与其他一元运算符相同

  • 大多数的 C++ 编译器都支持大部分通用的强制转换运算符:
    • (type) expression
    • type 是转换后的数据类型
  • C++ 支持的其他几种强制转换运算符:
    • const_cast<type> (expr):
      • const_cast 运算符用于修改类型的 const / volatile 属性。
      • 除了 constvolatile 属性之外,目标类型必须与源类型相同。
      • 这种类型的转换主要是用来操作所传对象的 const 属性,可以加上 const 属性,也可以去掉 const 属性。
    • dynamic_cast<type> (expr):
      • dynamic_cast 在运行时执行转换,验证转换的有效性。
      • 如果转换未执行,则转换失败,表达式 expr 被判定为 null
      • dynamic_cast 执行动态转换时,type 必须是类的指针、类的引用或者 void*
      • 如果 type 是类指针类型,那么 expr 也必须是一个指针,如果 type 是一个引用,那么 expr 也必须是一个引用
    • reinterpret_cast<type> (expr):
      • reinterpret_cast 运算符把某种指针改为其他类型的指针。
      • 它可以把一个指针转换为一个整数,也可以把一个整数转换为一个指针。
    • static_cast<type> (expr):
      • static_cast 运算符执行非动态转换,没有运行时类检查来保证转换的安全性。
      • 例如,它可以用来把一个基类指针转换为派生类指针。

模块

模块的概念:

  • 在C++中可以认为每个二进制文件为一个模块.

接口/跨模块接口:

  • 模块自己内部调用比较简单,因为编译环境和平台都一致,不存在不兼容的问题
  • 如果想把我们的功能提供给其他人使用,就需要导出接口和动态库文件了
  • 每种语言有自己的接口定义形式,接口在C或C++里就是一些头文件(.h),头文件里定义了结构体,函数等,供其他模块调用
  • 可以认为:模块由接口和二进制文件组成
    • windows编译出来的dll肯定不能在linux上调用,32位编译出的dll又不能被64位程序调用,Debug模式和Release模式也存在很多差异
    • C++不像Java,Java是编译一次在任意操作系统和平台都能跑起来;C++则是,不同操作系统,不同CPU,不同系统位数,甚至不同优化参数,编译出来的二进制文件都不能通用

动态库:

  • 一个”程序函数库”简单的说:就是一个文件包含了一些编译好的代码和数据,这些编译好的代码和数据可以在事后供其他的程序使用
  • 动态库就是编译好的,可供其他模块调用的二进制文件.在windows是dll形式,在类Unix是so形式
  • 动态库相比源码和静态库有以下优势: 1. 若以源码或静态库方式提供给别人使用,如果后期有一个bug需要修改,那么所有调用者都需要重新编译,测试,打包发布,成本很高. 2. 以动态库方式提供,使用者只需要替换dll或so即可,简单高效
  • 动态库的劣势: + 动态库版本维护比较麻烦,需要思考如何避免”dll地狱”

链接

  • 链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行.
  • 链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行

为了构造可执行文件,链接器必须完成两个主要任务:

  1. 符号解析(symbol resolution). 目标文件定义和引用符号,每个符号对应于一个函数,一个全局变量或一个静态变量.符号解析的目的是讲每个符号引用正好和一个符号定义关联起来
  2. 重定位(relocation). 编译器和汇编器生成从地址0开始的代码和数据节. 链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置.链接器使用汇编器产生的重定位条目(relocation entry)的详细指令,不加甄别地执行这样的重定位

关于链接器的一些基本事实:

  • 目标文件纯粹是字节块的集合
  • 这些块中,有些包含程序代码,有些包含程序数据,而其他的则包含引导链接器和加载器的数据结构
  • 链接器将这些块连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置
  • 链接器对目标机器了解甚少,产生目标文件的编译器和汇编器已经完成了大部分工作

目标文件有三种形式:

  1. 可重定位目标文件.包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件
  2. 可执行目标文件. 包含二进制代码和数据,其形式可以被直接复制到内存并执行
  3. 共享目标文件,一种特殊类型的可重定位目标文件,可以再加载或者运行时被动态地加载进内存并链接.
  • 编译器和汇编器生成可重定位目标文件(包含共享目标文件),链接器生成可执行目标文件.
  • 从技术上来说,一个目标模块(object module)就是一个字节序列,而一个目标文件(object file)就是一个以文件形式存放在磁盘中的目标模块.(不过,一般会互换地使用这些术语)
  • 目标文件是按照特定的目标文件格式来组织的,各个系统的目标文件格式都不相同.现代x86-64Linux和Unix系统使用可执行可链接格式(Executable and Linkable Format, ELF)

静态库:

  • “迄今为止,我们都是假设链接器读取一组可重定位目标文件,并把它们链接起来,形成一个输出的可执行文件”
  • 实际上,所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(static library)
  • 静态库可以用做链接器的输入,当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块.
  • 在Linux系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中.
  • 存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置.存档文件名由后缀.a标识.
  • 创建静态库需要用到一个工具:AR

链接器如何使用静态库来解析引用?

  • 在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件(驱动程序自动将命令行中所有的.c文件翻译为.o文件).
    • 在这次扫描中,链接器维护一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件)
    • 一个未解析的符号集合U(即引用了但是尚未定义的符号)
    • 一个在前面输入文件中已定义的符号集合D.(初始时,E,U和D均为空)
  • 解析的工作原理:
    1. 对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件.如果f是一个目标文件,那么链接器把f添加到E,修改UD来反应f中的符号定义和引用,并继续下一个输入文件
    2. 如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号.如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改UD来反应m中的符号定义和引用.对存档文件中所有的成员目标文件都依次进行这个过程,直到UD都不再发生变化.此时,任何不包含在E中的成员目标文件都简单地被丢弃,而链接器将继续处理下一个输入文件.
    3. 如果当链接器完成对命令行上输入文件的扫描后,U是非空的,那么链接器就会输入一个错误并终止.否则,它会合并和重定位E中的目标文件,构建输出的可执行文件.
  • 关于库的一般准则是将它们放在命令行的结尾

加载可执行目标文件:linux> ./program

  • 因为program不是一个内置的shell命令,所以shell会认为program是一个可执行目标文件,通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它.任何Linux程序都可以通过调用execve函数来调用加载器
  • 加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,这个将程序复制到内存并运行的过程叫做加载

共享库(shared library)

  • 共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的
  • 共享库也称为共享目标(share object),在Linux系统中通常用.so后缀来表示;微软的操作系统大量地使用了共享库,它们称为DLL(动态链接库)
  • 调用编译器驱动程序,给编译器和链接器相关指令:linux> gcc -shared -fpic -o libvector.so addvec.c multvec.c
    • -shared选项:指示链接器创建一个共享的目标文件
    • -fpic 选项:指示编译器生成与位置无关的代码
  • 在程序中调用共享库:linux> gcc -o prog21 main2.c ./libvector.so.
  • 基本的思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程.认识到这一点是很重要的:此时,没有任何libvector.so的代码和数据节真的被复制到可执行文件prog21中.反之,链接器复制了一些重定位和符号表信息,它们使得运行时可以解析对libvector.so中代码和数据的引用.

位置无关代码

  • 共享库的一个主要目的就是允许多个正在运行的进程共享内存中相同的库代码,因而节约宝贵的内存资源.
  • 可以接在而无需重定位的代码称为位置无关代码(Position-Independent Code, PIC),用户对GCC使用-fpic选项指示GNU编译系统生成PIC代码,共享库的编译必须总是使用该选项.

  • 处理目标文件的工具(GNU binutils包)
    1. AR: 创建静态库,插入,删除,列出和提取成员
    2. STRINGS: 列出一个目标文件中所有可打印的字符串
    3. STRIP: 从目标文件中删除符号表信息
    4. NM:列出一个目标文件的符号表中定义的符号
    5. SIZE: 列出目标文件中节的名字和大小
    6. READELF:显示一个目标文件的完整结构,包括ELF头中编码的所有信息,包含SIZE和NM的功能
    7. OBJDUMP:所有二进制工具之母,能够显示一个目标文件中所有的信息,它最大的作用是反汇编.text节中的二进制指令
    8. LDD:列出了一个可执行文件在运行时所需要的共享库.

小结

  • 链接可以在编译时由静态编译期来完成,也可以在加载时和运行时由动态链接器来完成.
  • 链接器处理称为目标文件的二进制文件,它有三种不同的形式:可重定位的,可执行的和共享的.
    1. 可重定位的目标文件由静态链接器合并成一个可执行的目标文件,它可以加载到内存中并执行
    2. 共享目标文件(共享库)是在运行时由动态链接器链接和加载的,或者隐含地在调用程序被加载和开始执行时,或者根据需要在程序调用dlopen库的函数时
  • 链接器的两个主要任务是符号解析和重定位.
    1. 符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,
    2. 重定位确定每个符号的最终内存地址,并修改对那些目标的引用
  • 静态链接器是由像GCC这样的编译驱动程序调用的,它们将多个可重定位目标文件合并成一个单独的可执行目标文件.多个目标文件可以定义相同的符号,而链接器用来悄悄地解析这些多重定义的规则可能在用户程序中引入微妙的错误
  • 多个目标文件可以被连接到一个单独的静态库中,链接器用库来解析其他目标模块中的符号引用,许多链接器通过从左到右的顺序扫描来解析符号引用,这是另一个引起令人迷惑的链接时错误的来源
  • 加载器将可执行文件的内容映射到内存,并运行这个程序.链接器还可能生成部分链接的可执行目标文件,这样的文件中有对定义在共享库中的例程和数据的未解析的引用.在加载时,加载器将部分链接的可执行文件映射到内存,然后调用动态链接器,它通过加载共享库和重定位程序中的引用来完成链接任务

交叉编译(cross compiler) 本地编译(native compiler)

Linaro

  • Linaro从2010年开始推动Arm上的开源软件开发,提供工具,Linux内核质量和安全性。
  • Linaro与成员公司和开源社区合作,维护Arm软件生态系统,在Arm架构上开拓新市场。

  • GCC的命令规则为:arch[-vendor][-os][-(gnu)eabi]-gcc
    • []是可选部分
    • arch : 芯片架构,例如32位的Arm架构对应的arch为arm,64位的Arm架构对应的arch为aarch64
    • vendor : 工具链提供商,大部分工具链名字里面都没有包含这部分
    • os : 编译出来的可执行文件(目标文件)针对的操作系统
  • 例如arm-linux-gnueabi-gcc, arm-none-eabi-gcc, aarch64-linux-gnu-gcc

vscode的launch.json文件:

  • name:调试的项目名
  • program:应用程序路径,这个最好放在共享目录,和板子使用同一个文件
  • cwd:程序源代码路径(重要)
  • miDebuggerPath:交叉编译工具中的gdb
  • miDebuggerServerAddress:远程gdbserver服务,根据设备对应修改

多核编程

  • 编译指导语句的含义是在编译器编译程序的时候,会识别特定的注释
  • 编译指导语句的形式为
    • #pragam omp <directive> [clause[[,] clause]. . .]
    • 其中directive部分就包含了具体的编译指导语句,包括parallel, for, parallel for, section, sections, single, master, critical, flush, orderedatomic
    • 这些编译指导语句或者用来分配任务,或者用来同步
    • 子句可以影响到编译指导语句的具体行为,每一个编译指导语句都有一系列适合它的子句。
    • 其中parallelforsectionssection等主要用来创建线程
  • 在C/C++程序中,用#pragma omp parallel来标志一段并行程序块

工程化代码和编程

  • 什么是编程和工程项目?
    • 编程(是编定程序的中文简称),就是让计算机代为解决某个问题,对某个计算体系规定一定的运算方式,使计算体系按照该计算方式运行,并最终得到相应结果的过程
    • 软件工程,(即咱们程序员经常口头说的项目),从软件开发的观点看,它就是使用适当的资源(包括人员,软硬件资源,时间等),为开发软件进行的一组开发活动,在活动结束时输入(即用户的需求)转化为输出(最终符合用户需求的软件产品)
  • 重要的三个阶段
    • 定义阶段:可行性研究初步项目计划、需求分析
    • 开发阶段:概要设计、详细设计、实现、测试
    • 运行和维护阶段:运行、维护、废弃
  • 编程和工程的区别?
    • 编程只是对某种问题的计算机解决方法,而工程却涉及到客户需求、人员、软硬件资源、时间、产品产出
    • 只有更好地了解整个工程流程,才能知道每个阶段需要干什么,客户需求什么,输出的产品是什么,才能轻松在给定时间内写出代码
  • 编写工程化代码需要注意:版本控制,接手项目,代码规范性和安全性,开发新项目

版本控制

  • 版本控制分三个部分——硬件资源环境、开发工具版本控制、程序版本控制

  • 硬件资源环境,
    • 不能只考虑自己电脑性能和程序能否在自己电脑运行。
    • 首先要确定运行工程代码的硬件环境是windows、还是linux、某个手机平台、或者arm开发板,其次确定是32位还是64位
  • 开发工具版本控制,
    • 只要和整个开发团队保持一致即可,
    • 如果只是你一个人使用的开发工具,不要追求最新、不稳定版本
  • 程序版本控制,这个最为重要。
    • 一般,公司都会有自己的版本控制服务器svn、github等,但我要说的是自己的程序版本控制,
    • 一般上传到服务器上的都是几经考验的稳定版本,但不排除有时候有些新修改漏斗没有觉察到或者自己需要恢复到某个阶段重新开发,这个时候有备份就至关重要了,
    • 所以每一个版本无论好坏都要给自己备份一下,有时候在关键时刻会助你一臂之力

接手项目

  • 接手项目,首先备份当前版本(版本控制的重要性)
  • 接着确认大环境,查看平台,动静态链接库是否齐全,配置文件是否还在,尝试编译运行一下能否通过,确认无误后再阅读代码,有问题了尽快请教。
  • 阅读代码时,先不要急于看每个功能如何实现,先把整个逻辑流程过一遍,找到自己业务的模块,再将该模块实现功能仔细阅读,并单独实现,最后再加以改进

  • 构思代码。为什么是构思代码,而不是写代码,因为直接写代码是没有灵魂的(大神除外)
  • 构思代码的好处是
    • 先把架构定下来,输入什么,输出什么,用什么数据结构和方法实现,
    • 这样不仅有助于接下来的编码,而且不容易反复推翻修改,三思而后动就是这个道理,这也有助于锻炼架构师思维

代码规范性和安全性

  • 很多大公司都有自己的一套编码规范。不论公司有没有规范,自己必须有一套符合大众的编码规范
  • 不仅代码看起来赏心悦目,方便问题查找,而且也便于以后阅读和维护,最重要的是代码可以烂,气势不能输

  • 代码的安全性分两个方面:代码自身的安全性和程序代码的安全性

  • 代码自身的安全性主要是指不要使用不安全的库函数、指针等
    • 编程语言也是代码,也会有bug,只是我们水平太菜,没发现而已。
    • 但是已经有很多前辈帮我们踏过这些雷区,我们需要的就是偶尔关注一下版本更替的说明和多看大神们的经验,避免使用那些具有安全隐患的库函数,
    • 尤其是在使用指针时,须慎重,如无必要,,不要耍酷(大神除外),血的教训告诉我指针引发的问题是最隐蔽的
  • 代码的安全性防范是写工程代码必须考虑的。
    • 程序代码的安全性就比较简单了,把自己的程序尽量封装成接口,这样既可以防止别人随意改动你的代码,也便于管理维护

开发新项目

  • 首先,分析需求,谨记一句话:如无必要,不要自作多情
    • 严格控制需求,仔细分析,清楚需求要做到什么程度,不要为了彰显自己半瓶水的功力,提升难度,补充需求
    • 本来让你做个人脸识别,你非要加个活体检测,结果GG了。学习研究可以挑战下自我,做工程就不必装×了
  • 其次,构思框架,三思而后行
    • 架构很重要,人有骨架,楼房也有钢筋,一个好的框架,可抗八级地震,减少二次开发的次数,提高运行效率,这些都不需要我们管,那是大佬构思的事情。
    • 我们要构思的是,如何采集数据,采集完数据,如何保存数据,分析数据,产出结果,结果如何展示给客户
  • 然后,确定环境

  • 接着,大环境确认了,就是详细设计
    • 输入什么,输出什么,数据格式,中间流经几个模块,实现方法,故障处理,实时性要求等等
    • 流程走下来,剩下就是填充代码和反复测试。最后,演示检验

谨记

  • 编程是练基本功的,工程是练思路和管理的,思路不行,永远只是个码农,基本功不扎实,思路再好也出不来干货,切勿眼高手低

C++11 lambda匿名函数

概述

  • lambda 源自希腊字母表中第 11 位的 λ,在计算机科学领域,它则是被用来表示一种匿名函数
  • 所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式

详解

  • lambda匿名函数的定义
    • [外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型 { 函数体; };
  • 其中各部分的含义分别为:
    • 1) [外部变量方位方式说明符]
      • [ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”。
      • 所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。
    • 2) (参数)
      • 和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;
    • 3) mutable
      • 此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。
      • 默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)
      • 而如果想修改它们,就必须使用 mutable 关键字。
      • 对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量;
    • 4) noexcept/throw()
      • 可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。
      • 默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。
      • 而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;
      • 使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。
      • 如果 lambda 函数标有 noexcept 而函数体内抛出了异常,又或者使用 throw() 限定了异常类型而函数体内抛出了非指定类型的异常,这些异常无法使用 try-catch 捕获,会导致程序执行失败
    • 5) -> 返回值类型
      • 指明 lambda 匿名函数的返回值类型
      • 如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略-> 返回值类型。
    • 6) 函数体
      • 和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。
      • 该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量
      • 外部变量会受到以值传递还是以引用传递方式引入的影响,而全局变量则不会。
      • 换句话说,在 lambda 表达式内可以使用任意一个全局变量,必要时还可以直接修改它们的值。
  • 定义了一个最简单的 lambda 匿名函数
    • []{}
    • 显然,此 lambda 匿名函数未引入任何外部变量([] 内为空),也没有传递任何参数,没有指定 mutable、noexcept 等关键字,没有返回值和函数体。
    • 所以,这是一个没有任何功能的 lambda 匿名函数。
  • lambda匿名函数中的[外部变量]
    • [] – 空方括号表示当前 lambda 匿名函数中不导入任何外部变量。
    • [=] – 只有一个 = 等号,表示以值传递的方式导入所有外部变量;
    • [&] – 只有一个 & 符号,表示以引用传递的方式导入所有外部变量;
    • [val1, val2, ...] – 表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序;
    • [&val1, &val2, ...] – 表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序;
    • [val, &val2, ...] – 以上 2 种方式还可以混合使用,变量之间没有前后次序。
    • [=, &val1, ...] – 表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。
    • [this]表示以值传递的方式导入当前的 this 指针
  • 注意:
    • 单个外部变量不允许以相同的传递方式导入多次。例如 [=,val1] 中,val1 先后被以值传递的方式导入了 2 次,这是非法的

lambda 的几种使用方式

  • Lambda 的定义使用以下语法:
    • [ capture list ] ( argument list ) -> return type { function body }
  • 解析
    • 捕获列表(capture list), 用于指定lambda中可访问的来自外部作用域的变量。变量可以通过值捕获、引用捕获或使用 this 捕获。
    • 参数列表(argument list)指定将递给 lambda 的参数。
    • 返回类型(return type)指定 lambda 将返回的值的类型。如果未指定,则编译器将尝试推断其类型。
    • 函数体(function body)指定 lambda 被调用时将执行的代码。
  • 以下是在C++中使用 lambda 的几种不同方式:
    • 函数回调(Function Callbacks)
    • 默认捕获(Capturing default)
    • 值捕获(Capturing by value)
    • 引用捕获(Capturing by reference)
    • 可变lambda(Mutable Lambdas)

函数回调

  • 函数回调是将一个函数作为参数传递给另一个函数,并在接收函数稍后的时间调用该函数。您可以将 lambda 作为函数参数传递,其中它将在发生某个事件时执行。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      #include <iostream>
      #include <algorithm>
      #include <vector>
    
      int main()
      {
          std::vector<int> vec = { 1, 2, 3, 4, 5 };
          int sum = 0;
          std::for_each(vec.begin(), vec.end(), [&sum](int x) { sum += x; });
          std::cout << "The sum is: " << sum << std::endl;
          return 0;
      }
    

默认捕获

  • 当一个 lambda 表达式被声明时没有任何显式的捕获,其默认行为是通过引用捕获周围作用域中的变量。这被称为默认捕获。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    
      #include <iostream>
    
      int main()
      {
          auto square = [](int x) { return x * x; };
          std::cout << "The square of 5 is: " << square(5) << std::endl;
          return 0;
      }
    

按值捕获

  • 这是 lambda 表达式的最简单形式,其中你通过值传递变量给函数。当一个变量被按值捕获时,它的当前值被存储在闭包中,而当周围作用域中的变量发生改变时,它的值不会被更新。这可以通过将变量包含在方括号 [ ] 中来实现。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    
      #include <iostream>
    
      int main() {
          int x = 42;
          auto f = [x](int y) { std::cout << x+y << std::endl;};
          f(1);
          return 0;
      }
    

按引用捕获

  • 你可以通过使用 & 符号将变量传递给 lambda 表达式来按引用捕获变量。当一个变量被按引用捕获时,它的当前值被存储在闭包中,并且在周围作用域中变量发生变化时被更新。这是通过在方括号[ ]中在变量前加上取地址运算符 & 来实现的。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    
      #include <iostream>
    
      int main() {
        int x = 42;
        auto f = [&x]() { std::cout << x << std::endl; };
        f();
        return 0;
      }
    

可变lambda表达式

  • 默认情况下,由 lambda 表达式捕获的变量是常量,不能在 lambda 表达式体内修改。如果你想要在 lambda 表达式中修改捕获的变量,你可以将 lambda 表达式设为可变。可变lambda允许捕获的变量被修改。这是通过在方括号 [ ] 中包含可变关键字来实现的。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    
      #include <iostream>
    
      int main() {
        int x = 42;
        auto f = [x]() mutable { std::cout << ++x << std::endl; };
        f();
        return 0;
      }
    

小结

  • lambda 表达式类似于普通函数,但它们有一些关键的区别。例如,lambda 表达式的类型没有被显式指定,但可以由编译器推断出来。此外,lambda 表达式可以从周围的作用域中捕获变量,这使得它们非常适用于创建闭包和在 C++ 中使用函数式编程概念。
  • 与传统函数相比,lambda 表达式具有一些性能优势:
    • 内联函数:编译器会自动将 lambda 表达式内联,这意味着它们的代码直接插入到调用函数中。这可以减少函数调用的开销并提高性能。
    • 避免命名函数的开销:lambda 表达式没有名称,因此它们不必被声明和存储在符号表中,这可以减少开销并提高性能。
    • 改善高速缓存局部性:lambda 表达式可以在同一个函数中定义和使用,这意味着lambda使用的代码和数据将存储在与调用代码相同的高速缓存行中。这可以改善高速缓存局部性并降低高速缓存失效的成本。
    • 减小代码大小:lambda 表达式通常比命名函数小,并且它们不需要外部函数调用,这可以减小编译代码的大小并提高性能。
    • 增加灵活性:lambda 表达式可以用来将函数作为参数传递给其他函数,这提供了更大的灵活性,可以改善性能,减少重复代码的需求。
    • 提高可读性:lambda 表达式可以通过以紧凑而自包含的方式封装复杂的逻辑来使代码更易于阅读。这可以通过使代码更易于理解和维护来提高性能。

const 小结

  • const 是constant的缩写,本意是不变的,不易改变的意思。
  • const 在C++中是用来修饰内置类型变量,自定义对象,成员函数,返回值,函数参数

const修饰普通类型的变量

  • 对于const变量a,我们取变量的地址并转换赋值给 指向int的指针,然后利用*p = 8
  • Volatile关键字跟const对应相反,是易变的,容易改变的意思。所以不会被编译器优化,编译器也就不会改变对a变量的操作。

const 修饰指针变量

  • const 修饰指针变量有以下三种情况
    • const 修饰指针指向的内容,则内容为不可变量。
    • const 修饰指针,则指针为不可变量。
    • const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。
  • const int *p = 8;
    • 则指针指向的内容8不可改变。简称左定值,因为const位于*号的左边。
  • int* const p = &a;
    • 对于const指针p其指向的内存地址不能够被改变,但其内容可以改变。简称,右定向。因为const位于*号的右边。
  • const int * const p = &a;
    • 这时,const p的指向的内容和指向的内存地址都已固定,不可改变。

const参数传递和函数返回值

  • 对于const修饰函数参数可以分为三种情况
    • 值传递的const修饰传递,一般这种情况不需要const修饰,因为函数会自动产生临时变量复制实参值。
    • 当const参数为指针时,可以防止指针被意外篡改。
    • 自定义类型的参数传递,需要临时对象复制参数,对于临时对象的构造,需要调用构造函数,比较浪费时间,因此我们采取const外加引用传递的方法。并且对于一般的int ,double等内置类型,我们不采用引用的传递方式。
  • Const修饰返回值分三种情况
    • const 修饰内置类型的返回值,修饰与不修饰返回值作用一样。
    • const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
    • const 修饰返回的指针或者引用,是否返回一个指向const的指针,取决于我们想让用户干什么。

const修饰类成员函数

  • 只有类的成员函数才能在函数名后面加上 const ,这时成员函数称为 常量成员函数
  • 常量成员函数在执行期间不能修改成员变量的值(静态成员变量除外),也不能调用同一个类中的非常量成员函数(同样的静态成员函数除外)

  • const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为const成员函数。
  • 注意:
    • const关键字不能与static关键字同时使用,因为static关键字修饰静态成员函数,静态成员函数不含有this指针,即不能实例化,const成员函数必须具体到某一实例
  • 如果有个成员函数想修改对象中的某一个成员怎么办?
    • 这时我们可以使用mutable关键字修饰这个成员,
    • mutable的意思也是易变的,容易改变的意思,被mutable关键字修饰的成员可以处于不断变化中

override 重载

  • 当在父类中使用了虚函数的时候,可能需要在某个子类中对这个虚函数进行重写,以下方法都可以:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      class A
      {
          virtual void foo();
      }
    
      class B :public A
      {
          void foo(); //OK
          virtual foo(); // OK
          void foo() override; //OK
      }
    
  • 如果不适用 override, 当把foo()写成foO()时,编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个函数很重要的话,那就会对整个程序不利
  • 所以,override的作用就出来了: 它指定了子类的这个虚函数是重写的父类的。如果名字不小心打错了,编译器是不会编译通过的,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      class A
      {
          virtual void foo();
      };
    
      class B :A
      {
          virtual void f00(); //OK
          virtual void f0o()override; //Error 
      };
    
  • 为了减少程序的运行时错误,还是养成重写虚函数加上 override 的习惯

final

  • 简介:
    • final 是 C++11 引入的一个关键字,用于在类、成员函数或虚函数声明中指示其为最终版本,不可被派生类继承或覆盖
    • 在类声明中,final 关键字用于阻止其他类继承该类。如果将 final 关键字应用于类声明,则该类将成为最终类,不能被其他类继承
  • 示例1: ```cpp class Base final { // 类定义 };

class Derived : public Base { // 错误,Derived 不能继承自 final 类 Base // 类定义 };

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

+ 注1:
  + 在上述示例中,Base 类被声明为 final 类,因此不能被其他类继承。当 Derived 类试图从 Base 类派生时,会导致编译错误。
  + 在成员函数声明中,final 关键字用于阻止派生类重写该函数。如果将 final 关键字应用于虚函数或重载函数,它表示该函数在派生类中不能被覆盖

+ 示例2:
```cpp
class Base {
public:
    virtual void foo() final {
        // 函数定义
    }
};

class Derived : public Base {
public:
    void foo() override { // 错误,不能覆盖被声明为 final 的函数 foo
        // 函数定义
    }
};
  • 注2:
    • 在上述示例中,Base 类中的 foo() 函数被声明为 final,表示它不能在派生类中被覆盖。当 Derived 类试图覆盖 foo() 函数时,会导致编译错误。
    • 使用 final 关键字可以提供额外的语义信息,以确保类或函数的最终性。它可以用于设计层面的约束,防止意外的继承或覆盖,从而提高代码的安全性和可靠性
    • 请注意,final 关键字仅在派生类和虚函数/重载函数中使用,不能用于其他上下文。此外,final 关键字是 C++11 引入的特性,因此需要使用支持 C++11 或更高版本的编译器

Linux C++与Windows C++

1.1

  • 我之所以把这个标题单独列出来,是想纠正现在很多 C/C++ 新人和初学者一些不当的认识,一般有以下几种观点:
    • Linux C++ 开发就是后台开发,而 Windows C++ 开发就是客户端开发;
    • 后端开发比客户端开发(前端)高级,因此后端开发行业薪资水平比客户端开发薪资要高;
    • 我只学 Linux,不学 Windows。
  • Linux C++ 和 Windows C++ 一样,没有孰高孰低之分,只是两种不同的操作系统而已,不要觉得在 Linux 下敲命令就比在 Windows 的图形化界面点击鼠标达高级。
  • 图形化界面之于命令行,是人们对更高级、更方便的工具追求的必然结果。Linux C++ 也不一定就是后台开发,Windows C++ 也不一定就是客户端开发;所谓的服务器与客户端是个相对的概念,即谁给谁提供服务,提供服务的我们认为是服务端(后台),被服务的我们认为是客户端(前台)。而 Windows 作为后台服务的应用也比比皆是,如笔者之前所在的某交易所的服务器后台都是 Windows 下的 C++ 程序;另外如一些游戏类的服务器端,也不少是 Windows 的。
  • 借用《UNIX 编程艺术》这本书的观点,Windows 和 Linux 的哲学理念不一样,Windows 是假设你不会操作,它教你如何操作,而 Linux 是假设你会操作然后进行操作。根据这个理念,Windows 一般是普通人用的多,而 Linux 是程序员用的多。
  • 从编程的角度来说,Windows 的代码风格是所谓的匈牙利命名法,而 Linux 是短小精悍的连字符风格。例如同一个表示屏幕尺寸的整型变量,Windows 上可能被命名为 iScreen 或 cxScreen ,而 Linux 可能是 screen;再例如 Windows 上创建线程的函数叫 CreateThread,Linux 下叫 pthread_create。有时候,我觉得 Windows 的匈牙利命名法反而更容易理解代码。

  • 这里既然提到前端(客户端)开发和后端开发,这里不得不提一下,这二者没有优劣之分。其侧重点和开发思维是不一样的,
    • 前端(客户端)开发一般有较多的界面逻辑,它们是直接与用户打交道,因而一款客户端软件的好坏很大程度上取决于其界面的易用性和流畅性,开发者只要把这一端的“一亩三分地”给管理好即可;
    • 而后端服务,对于普通用户是透明的,开发者的程序必须尽量体现“服务”这个字眼,即更有效地为更多的客户端服务,这就要求兼顾请求响应的正确性、及时性和流畅性。
  • 由于服务软件也是运行在某台物理机器上的程序,鉴于 CPU、内存、网络带宽资源有限,而服务程序一般是长周期运行的,因此必须合理地分配和使用资源(如尽量回收不再使用的各种资源)。开发者应从全局考虑,不能在某个“客户端”这一棵树上“吊死”。
  • 从个人的职业发展来看,建议从事客户端开发的人员适当地了解一下服务器开发的思路,反过来也建议从事后端开发的人员去学习一下客户端开发,二者相得益彰。从个人的技术提高来说,也是很有帮助的。
  • 例如您要学习一套开源的软件代码,如果您熟悉客户端和服务器的基本开发和调试技巧,您可以更好地学习它。而在工作上,一个项目,往往是由客户端和服务器程序组成,如果您都熟悉,您可以站在一个更高的角度去审视它、规划它,这也是架构师的基本要求之一。

如何看待 C++ 11/14/17 新标准

1.1

  • C++ 开发者有个不成文的规定:即使您对 C++ 很熟悉,也不要在简历上写上您精通 C++,原因很简单—— C++ 这门语言包含的东西实在太多了,没有人能真正“精通”所有。
  • C++ 既支持面向对象设计(OOP),也支持以模板语法为代表的泛型编程(GP)。而且新的 C++ 标准和遵循 C++ 新标准的编译器也层出不穷,这些年,C++ 变化越来越大、越来越快,从最初业界和开发者翘首以盼的 C++11 标准,历经 C++14、C++17 到今天的 C++20,这门语言与之前的版本差别越来越大,更多原来需要使用第三库的功能也被陆续添加到 C++ 标准库中。

  • 就我个人经验来说,对于C++11、C++14、C++17 乃至 C++20,我们学习它们的准则应该是以实用为主,也就是说我们应该学习其实用的部分,至于新标准提到的一些高级特性和各种复杂的模板,我们大可不必去了解。我们并不是做学术研究,我们学习 C++ 是为了投入实际的生产开发,所以应该去学习 C++ 新标准中实用的语法和工具库。关于 C++11 常用一些知识点,这里也简单地给读者列举一下。
    • auto 关键字
    • for-each 循环
    • 右值及移动构造函数 + std::forward + std::move + stl 容器新增的 emplace_back() 方法
    • std::thread 库、std::chrono 库
    • 智能指针系列(std::shared_ptr/std::unique_ptr/std::weak_ptr),智能指针的实现原理一定要知道,最好是自己实现过
    • 线程库 std::thread + 线程同步技术库 std::mutex/std::condition_variable/std::lock_guard 等
    • Lamda 表达式(Java 中现在也常常考察 Lamda 表达式的作用)
    • std::bind/std::function 库
    • 其他的就是一些关键字的用法(override、final、delete),还有就是一些细节如可以像 Java 一样在类成员变量定义处给出初始化值。

C++ 语言基础与进阶

  • 这里说的基础不是狭义上的 C++ 语言基础,而是包括 C++ 开发这一生态体系的基础,笔者认为的基础包括:
    • C++ 语言本身熟练使用程度。
    • 前面也介绍了单纯的 C++ 您啥也干不了,您必须结合一个具体的操作系统平台,所以得熟悉某个操作系统平台的 API 函数,比如Linux,以及该操作系统的原理。这里说的操作系统的原理不局限于您在操作系统原理图书上看的知识,而是实实在在与系统 API 关联起来的,如熟练使用各种进程与线程函数、多线程资源同步函数、文件操作函数、系统时间函数、窗口自绘与操作函数(这点针对 Windows)、内存分配与管理函数、PE 或 ELF 文件的编译、链接原理等等。
    • 网络通信,网络通信在这里具体一点就是 Socket 编程。这里的 Socket 编程不仅要求熟练使用各种网络 API 函数,还要求理解和灵活运用像三次握手四次挥手等各种基础网络通信协议与原理。关于 Socket 编程实践,《TCP/IP 网络编程》这本书是非常好的入门教材。
  • 总结起来,可以得到如下公式:
    • 一款 C++ 软件 = C++ 语法 + 操作系统 API 函数调用
  • 如果您达到了我上面说的三点后,可以再找一些高质量的开源项目去实战一下。需要注意的是,最好找一些没有复杂业务或者您熟悉其业务的开源项目(如开源的 IM 系统)。如果你不熟悉其业务,不仅要学习其业务(软件功能),还需要再去学习它的源码,最后可能让我们迷失了最初学习这款软件的目的。
  • 学习这些项目的同时,读者应该先确定自己的学习目的,如果您的目的是学习和借鉴这款软件的架构,那么先从整体去把握,不要一开始就迷失在细枝末节中,这类我称之为“粗读”;或者您的目的是学习开源软件在一些细节上的处理与做法,这个时候,您可以针对性地去阅读您感兴趣的模块,深入到每一行代码上

  • 学习开源软件存在一种风气,许多新手喜欢道听途说,一听别人说这个软件不好,那个软件存在某某瑕疵就放弃阅读它的打算了。然后到了实际开发中,因为心中没有任何已有软件开发问题的解决方案,产生挫败感,久而久之就对本来喜欢的 C/C++ 开发失去了兴趣。
  • 学习的过程是先接触,再熟悉,再模仿,再创造。不管什么开源项目,在您心中没有任何思路或者解决方案时,您应该先接触熟悉,不断模仿,做到至少心中有一套对于某场景的解决方案,然后再来谈创新谈批判、改造别人的项目。
  • 我个人学习一套陌生的开源项目时,总是喜欢将程序用调试器正常跑起来,然后再中断下来,统计当前的线程数目,然后通过程序入口 main 函数从主线程追踪其他工作线程是如何创建的;接着,分析和研究每个线程的用途以及线程之间交互的,这就是整体把握,接着找我感兴趣的细节去学习。

C++面试

  • 对于社会人士参加的 C++ 职位的面试,如果是大型互联网公司,虽然社会招聘问的更多的是项目经验,适当地为一些基础的算法和数据结构知识做一些准备也是非常有用的。举个例子,如果问到二分查找这一类基础算法,如果答不出来未免会让面试官印象不太好,场面也比较尴尬。另外,C++ 是一门讲究深度的编程技能,对于有一定工作年限的面试者,面试官往往会问很多原理性的细节,这就要求广大 C++ 开发者在平常多留心、多积累、多思考技术背后的原理。
  • 对于大多数小型企业,无论是应届生还是社会人士,只要有能力胜任一定的工作即可。一般只要对所面试的公司项目有类似经验或者相关的技术能力,基本上就可以通过面试。大多数小公司在乎的是您来了能不能干活,所以这类公司对实际项目经验和技能要求更高一点。
  • 关于项目经验,许多人可能觉得项目经验一定是自己参与的项目,其实不然,项目经验也可以来源于您阅读和学习其他人的项目代码或者开源软件,只要您能看懂并理解它们,在面试的时候提及到,能条理清晰、自圆其说即可。当然,如果不熟悉或者只是了解些皮毛,切记不可信口开河、胡编乱造甚至张冠李戴。
  • 我曾经面试过一些开发者,看简历项目经验丰富,实际一问的时候,只是把别人的框架或者库拿来包装调用一下,问及其技术原理时,不是顾左右而言他就是说不清道不明模棱两可含糊不清,这一类人往往比不知道还让人讨厌,面试官一般反感这一类面试者所谓的项目经验。

学生与社会人士学习 C++ 方式的区别

  • 作为学生有充裕的时间,建议除了把 C++ 语法学好,系统地多读一点基础的书籍,如操作系统原理、网络编程、数据结构与算法等相关各方各面的经典书籍。
  • 社会人士由于已经走上工作岗位,家庭、工作的琐事繁多,没有太多的时间去系统地阅读一些相关基础书籍,如果您当前工作正好是从事 C/C++ 开发,那么请结合您当前的项目来学习,搞清楚项目的体系结构、吸收项目中优秀的实现细节,针对性地补充相关知识,这是进步最快的方式。
  • 但是实际情形中,很多人觉得公司的项目代码又烂又杂,不愿意去研究。这种思想千万不能有的,在您没有自己足够好的能力给公司提供更好的解决方案,请先学习和模仿,我们此时要保持“空杯”心态,公司的代码再烂,它也是公司的商业价值所在;即使是纯粹的业务代码,也有它的可取之处,择其善者而从之,其不善者而改之。尤其是开发者处于一些初中级的开发岗位时,可能接触不到公司核心框架的源码,此时千万不要盲目地去排斥。学业务,补基础,时刻意识清醒自己所需,明白自己想要学的东西。
  • 如果从事的不是 C++ 相关的开发,那么可以挤出一些时间去学习一些开源的代码,在阅读开源代码的过程中,针对性地补缺补差。不建议系统地去看《C++ Primer 中文版》《UNIX 环境高级编程》诸如此类的大部头书籍,实际开发中不需要太多这类书中的细枝末节,阅读这类书往往只会事倍功半,甚至最后因书籍太厚、时间不够,最后坚持不下去,最终放弃。

  • 当然,对于社会人士,当您有一定的时间的时候一定要去补充一些基础的、原理性的东西,千万不要沉溺于“面向搜索引擎编程”或者“面向工资编程”,有些问题虽然当时通过搜索引擎解决了,但如果想在技术或职业上有长足的发展,一定要系统地去读一些经典的、轻量级的书籍(如《C++ 对象模型》)。长期在网上的文章中寻章摘句,只会让您的知识结构碎片化、凌乱化,甚至混乱化。而且互联网上的技术文章质量良莠不齐,有时候也容易对自己形成误导和依赖。总而言之,作为技术开发人员,提高自己技术水平是改变现状、改善生活最直接的途径。

  • 关于 C/C++,暂且就讨论这么多。最后再强调一遍,C++ 是一门讲究深度的语言,其“深度”不是体现在会多少 C++ 语法,而是能够洞察您所写的 C++ 代码背后的系统原理,这是需要长期不断的积累的,没有速成之法。反过来一旦学成,可以快速地学习其他语言和框架。个人觉得,如果自主创业或者想在二三线城市长期发展的读者,C/C++ 应该是优选语言,有了它作为基础,您可以跳出依赖各种环境和框架的窠臼,快速地学习和开发您想要的软件,完成您想要的业务产品。

回调函数

笔记-0

  • 参考:
    • https://www.cnblogs.com/swordzj/archive/2007/04/24/2034769.html

1.1 什么是回调?

  • 软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用
    • 同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;
    • 回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;
    • 异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)
  • 回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。
  • 同步调用是三者当中最简单的,而回调又常常是异步调用的基础,
  • 对于不同类型的语言(如结构化语言和对象语言)、平台(Win32、JDK)或构架(CORBA、DCOM、WebService),客户和服务的交互除了同步方式以外,都需要具备一定的异步通知机制,让服务方(或接口提供方)在某些情况下能够主动通知客户,而回调是实现异步的一个最简捷的途径。
  • 对于一般的结构化语言,可以通过回调函数来实现回调。回调函数也是一个函数或过程,不过它是一个由调用方自己实现,供被调用方使用的特殊函数。
  • 在面向对象的语言中,回调则是通过接口或抽象类来实现的,我们把实现这种接口的类成为回调类,回调类的对象称为回调对象。对于像C++或Object Pascal这些兼容了过程特性的对象语言,不仅提供了回调对象、回调方法等特性,也能兼容过程语言的回调函数机制。
  • Windows平台的消息机制也可以看作是回调的一种应用,我们通过系统提供的接口注册消息处理函数(即回调函数),从而实现接收、处理消息的目的。由于Windows平台的API是用C语言来构建的,我们可以认为它也是回调函数的一个特例。
  • 对于分布式组件代理体系CORBA,异步处理有多种方式,如回调、事件服务、通知服务等。事件服务和通知服务是CORBA用来处理异步消息的标准服务,他们主要负责消息的处理、派发、维护等工作。对一些简单的异步处理过程,我们可以通过回调机制来实现。

1.2 面向对象语言中的回调

  • 对于回调的实现,也有两种截然不同的模式:
    • 一种是结构化的函数回调模式
    • 一种是面向对象的接口模式
  • 回调对象,什么叫做回调对象?它具体用在那些场合?
    • 首先,让我们把它与回调函数对比一下,回调函数是一个定义了函数的原型,函数体则交由第三方来实现的一种动态应用模式。要实现一个回调函数,我们必须明确知道几点:该函数需要那些参数,返回什么类型的值。
    • 同样,一个回调对象也是一个定义了对象接口,但是没有具体实现的抽象类(即接口)。要实现一个回调对象,我们必须知道:它需要实现哪些方法,每个方法中有哪些参数,该方法需要放回什么值。
  • 因此,在回调对象这种应用模式中,我们会用到接口。接口可以理解成一个定义好了但是没有实现的类,它只能通过继承的方式被别的类实现。

  • 回调方法(Callback Method),可以看做是回调对象的一部分。
    • 在有些场合,我们不需要按照给定的要求实现整个对象,而只要实现其中的一个方法就可以了,这时我们就会用到回调方法

笔记-1

  • 参考:
    • https://www.cnblogs.com/cyyz-le/p/11278903.html

1.1 回调函数的作用

  • 官方定义:
    • 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
  • 函数指针:
    • 函数指针也是一种指针,只是它指向的不是整型,字符型,而是函数。在C中,每个函数在编译后都是存储在内存中,并且每个函数都有一个入口地址,根据这个地址,我们便可以访问并使用这个函数。函数指针就是通过指向这个函数的入口,从而调用这个函数。
  • int func();, int (*p)();
    • func是一个返回值为整型的函数
    • p是一个指针,指针指向一个函数,函数的返回值是整型
    • p = func;,函数func把地址赋给函数指针p
    • 下一次调用func()的时候,可以直接调用(*p)()或者p(),为什么可以直接使用p()?
      • 因为函数名本质也是一个地址,函数指针本质也是一个地址,把地址func赋给了pp就等于func
      • (*p)()p()相当于间接访问和直接访问的关系,不用纠结过多
  • 回调函数:
    • 回调函数就是你写一个函数,把函数地址赋值一个函数指针,然后把这个函数指针当作参数赋给另一个函数,另一个函数通过函数指针的地址调用这个函数,就是回调函数

笔记-2

  • 参考:
    • https://www.cnblogs.com/danshui/archive/2012/01/02/2310114.html

1.1 概述

  • 回调函数,就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

  • 回调函数机制:
    • 定义一个函数(普通函数即可)
    • 将此函数的地址注册给调用者
    • 特定的事件或条件发生时,调用者使用函数指针调用回调函数
  • 注意:
    • 为什么要特定事件或条件发生?
    • 不应该随时都可以调用回调函数吗?
    • 可以把回调函数和调用函数封装承类再调用

1.2 函数指针

  • 概念:
    • 指针是一个变量,是用来指向内存地址的。
    • 一个程序运行时,所有和运行相关的物件都是需要加载到内存中,这就决定了程序运行时的任何物件都可以用指针来指向它。
    • 函数是存放在内存代码区域内的,它们同样有地址,因此同样可以用指针来存取函数,把这种指向函数入口地址的指针称为函数指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  void Invoke(char* s);

  int main()
  {
      void (*fp)(char* s);    //声明一个函数指针(fp)        
      fp=Invoke;              //将Invoke函数的入口地址赋值给fp
      fp("Hello World!\n");   //函数指针fp实现函数调用
      return 0;
  }

  void Invoke(char* s)
  {
      printf(s);
  }
  • 由上可知:
    • 函数指针函数的声明之间唯一区别就是,用指针名(*fp)代替了函数名Invoke,这样这声明了一个函数指针,然后进行赋值fp=Invoke就可以进行函数指针的调用了。
    • 声明函数指针时,只要函数返回值类型、参数个数、参数类型等保持一致,就可以声明一个函数指针了。
    • 注意,函数指针必须用括号括起来 void (*fp)(char* s)
  • 实际中,为了方便,通常用宏定义的方式来声明函数指针,实现程序如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      typedef void (*FP)(char* s);
      void Invoke(char* s);
    
      int main(int argc,char* argv[])
      {
          FP fp;      //通常是用宏FP来声明一个函数指针fp
          fp=Invoke;
          fp("Hello World!\n");
          return 0;
      }
    
      void Invoke(char* s)
      {
          printf(s);
      }
    

1.3 回调函数 - 详解

  • 概念:
    • 回调函数,顾名思义,就是使用者自己定义一个函数,使用者自己实现这个函数的程序内容,然后把这个函数作为参数传入别人(或系统)的函数中,由别人(或系统)的函数在运行时来调用的函数。
    • 函数是你实现的,但由别人(或系统)的函数在运行时通过参数传递的方式调用,这就是所谓的回调函数。简单来说,就是由别人的函数运行期间来回调你实现的函数
  • 示例:
    • 标准Hello World程序:
      1
      2
      3
      4
      5
      
        int main(int argc,char* argv[])
        {
            printf("Hello World!\n");
            return 0;
        }
      
    • 将它修改成函数回调样式:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
        //定义回调函数
        void PrintfText() 
        {
            printf("Hello World!\n");
        }
      
        //定义实现回调函数的"调用函数"
        void CallPrintfText(void (*callfuct)())
        {
            callfuct();
        }
      
        //在main函数中实现函数回调
        int main(int argc,char* argv[])
        {
            CallPrintfText(PrintfText);
            return 0;
        }
      
    • 修改成带参的回调样式:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
        //定义带参回调函数
        void PrintfText(char* s) 
        {
            printf(s);
        }
      
        //定义实现带参回调函数的"调用函数"
        void CallPrintfText(void (*callfuct)(char*),char* s)
        {
            callfuct(s);
        }
      
        //在main函数中实现带参的函数回调
        int main(int argc,char* argv[])
        {
            CallPrintfText(PrintfText,"Hello World!\n");
            return 0;
        }
      

C++11中变量初始化方法汇总

  • 参考资料:https://blog.csdn.net/Q1302182594/article/details/47423347

1.1 背景

  • 在C++语言中,初始化 和 赋值 并不是同一个概念
    • 初始化:创建变量时赋予其一个初始值
    • 赋值:把对象(已经创建)的 当前值 擦除,而用一个新值来代替

1.2 列表初始化

  • 作为C++11新标准的一部分,用花括号来初始化变量得到了全面应用(在此之前,只是在初始化数组的时候用到)
  • 列表初始化有两种形式:
    • int a = {0}; // 列表初始化方式1
    • int a{0}; // 列表初始化方式2
  • 上述的两种方式都可以将变量a初始化为0

  • 当对内置类型使用列表初始化时,若初始值存在丢失的风险,编译将报错,如:
    1
    2
    
      int a = 3.14;   // 正确,虽然会丢失小数部分,但是编译器不报错。
      int a = {3.14}; // 错误,因为将会丢失小数部分(其实,g++只是对此提示警告而已,并非错误)。
    
  • 由花括号括起来的初始值,不仅可以用于初始化变量,还可以用于为对象(旧变量)赋新值。
    1
    2
    
      int a = 1;   // 定义变量,并且初始化为1
      a = {3};     // 为变量a赋新值3
    
  • 用处:
    • 可以用在任何需要变量初始化的地方,例如第6章的类成员初始化,以及在for()中定义的变量:
      1
      2
      3
      
        for (int i{0}; i < 10; i++) {
        ...
        }
      

1.3 拷贝初始化

  • 如果使用等号初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去
  • 示例:
    1
    2
    
      string s1;       // 默认初始化为空字符串
      string s2 = s1;  // 拷贝初始化,s2是s1的副本
    

1.4 直接初始化

  • 如果在新创建的变量右侧使用括号将初始值括住(不用等号),则执行的是直接初始化(direct initialization)
    1
    2
    3
    
      string s1();        // 直接初始化为空字符串 
      string s2("hi");    // 直接初始化
      string s3(3, 'c');  // 直接初始化,s2的内容是ccc
    
  • 使用圆括号提供初值是用来构造(construct)对象,因此可以知道,所谓的直接初始化就是显式的调用相应的构造函数

  • 值初始化的三种情况
    • 在数组初始化过程中如果提供的初值数量少于数组大小时;
    • 当不使用初始值定义一个局部静态变量时;
    • 当通过T()形式的表达式显式地请求值初始化时(T是类型名)
  • explicit构造函数只能用于直接初始化

std::cout 输出流和字符指针

  • 相关知识:
    • C-C++01_语言基础.md: 字符串 – C++

std::cout 输出流

  • std::cout语句的一般格式为:std::cout << 表达式1 << 表达式2 << …… << 表达式n;
  • std::cin语句的一般格式为:std::cin >> 变量1 >> 变量2 >> …… >> 变量n;

  • 在定义流对象时,系统会在内存中开辟一段缓冲区,用来暂存输入输出流的数据。
  • 在执行std::cout语句时,先把插入的数据顺序存放在输出缓冲区中,直到输出缓冲区满或遇到std::cout语句中的std::endl(或'\n',ends,flush)为止,此时将缓冲区中已有的数据一起输出,并清空缓冲区。输出流中的数据在系统默认的设备(一般为显示器)输出。

字符指针

  • 以字符串形式出现的,编译器都会为该字符串自动添加一个0作为结束符,如在代码中写:"abc",那么编译器帮你存储的是"abc\0"

  • "abc"是常量吗?答案是有时是,有时不是。

    • 不是常量的情况:"abc"作为字符数组初始值的时候就不是,如char str[] = "abc";
      • 因为定义的是一个字符数组,所以就相当于定义了一些空间来存放"abc",而又因为字符数组就是把字符一个一个地存放的,所以编译器把这个语句解析为 char str[3] = {‘a’,’b’,’c’};又根据上面的总结1,所以char str[] = “abc”;的最终结果是 char str[4] = {‘a’,’b’,’c’,’\0’};
      • 做一下扩展,如果char str[] = “abc”;是在函数内部写的话,那么这里的”abc\0”因为不是常量,所以应该被放在栈上
    • 是常量的情况: 把"abc"赋给一个字符指针变量时,如char* ptr = "abc";
      • 因为定义的是一个普通指针,并没有定义空间来存放"abc",所以编译器得帮我们找地方来放"abc",显然,把这里的"abc"当成常量并把它放到程序的常量区是编译器最合适的选择。
      • 所以尽管ptr的类型不是const char*,并且ptr[0] = ‘x’;也能编译通过,但是执行ptr[0] = ‘x’;就会发生运行时异常,因为这个语句试图去修改程序常量区中的东西。
      • 记得哪本书中曾经说过char* ptr = “abc”;这种写法原来在c++标准中是不允许的,但是因为这种写法在c中实在是太多了,为了兼容c,不允许也得允许。虽然允许,但是建议的写法应该是const char* ptr = “abc”;这样如果后面写ptr[0] = ‘x’的话编译器就不会让它编译通过,也就避免了上面说的运行时异常
      • 又扩展一下,如果char* ptr = “abc”;写在函数体内,那么虽然这里的”abc\0”被放在常量区中,但是ptr本身只是一个普通的指针变量,所以ptr是被放在栈上的, 只不过是它所指向的东西被放在常量区罢了。

  • 字符数组和字符指针最根本的区别是:
    • 在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限

使用std::cout输出字符串指针地址

  • 由于C++标准库中I/O类对«操作符重载,因此在遇到字符型指针时会将其当作字符串名来处理,输出指针所指的字符串。
  • 既然这样,那么我们就别让它知道那是字符型指针,所以得用到强制类型转换,不过不是C的那套,我们得用static_cast来实现,把字符串指针转换成无类型指针,这样更规范,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      #include <iostream>
    
      int main()
      {
        const char *ptr = "this is a string";
    
        // C
        printf("string value is : %s \n", ptr);
        printf("the pointer of string is : %p \n", ptr);
    
        // C++
        std::cout << "string value is :" << ptr << std::endl;
        std::cout << "the pointer of string is :" << static_cast<const char *>(ptr) << std::endl;
    
        return 0;
      }
    

c++中函数参数返回值用std::string好还是const char *

函数参数传递

1.1

  • 有这样一个函数test需要两个字符串作为参数,那么test的原型定义成test(string, string)呢还是定义成test(const char*, const char*)还是其他呢?

  • 当这样使用时 test(“hello”, “world”):
    • 如果原型是第一种test(string, string),就需要首先构建string对象,然后因为是值传递需要内存拷贝。
    • 如果原型是第二种test(const char*, const char*),由于需求是需要两个string型变量,参数传进来之后还得转换成string型,如:string a(A);string b(B);
  • 这也不省事,如果用另外一种就非常好了,就是传递string引用,原型声明为test(const string& A, const string& B):
    • 字符串传递进来时,并没有发生内存拷贝,效率会很高,并且AB可以直接当成string型来用,也比第二种方便。

1.2

  • 如果该函数是作为接口给其他人使用(非源码级),那么使用const char*,比如lib或dll
  • 如果函数内部使用的是const char*,不会转换为std::string,函数调用方也是const char*,那么使用const char*
  • 其它情况都用const std::string&

函数返回值

  • 关于函数的返回值,最好还是返回“值”吧,最好不要返回地址啊引用啊什么的,因为你不清楚使用者会不会改变这些值,如果返回的是引用,内存中只有一份,如果用户修改了值,势必会影响到其他的。
  • 所以这个函数的原型最好是 string test(const string& A, const string& B)

std::vector 技巧

使用std::vector时,有几个技巧可以帮助您更有效地使用和操作向量。以下是一些常用的std::vector技巧:

  1. 初始化和赋值:
    • 使用初始化列表初始化向量:std::vector<int> vec = {1, 2, 3, 4, 5};
    • 使用范围构造函数从另一个容器初始化向量:std::vector<int> vec(otherVec.begin(), otherVec.end());
    • 使用assign()函数从另一个容器或范围进行赋值:vec.assign(otherVec.begin(), otherVec.end());
  2. 动态调整大小:
    • 使用resize()函数调整向量大小,并在需要时插入或删除元素:vec.resize(newSize);
    • 使用push_back()函数在向量末尾插入元素:vec.push_back(value);
    • 使用pop_back()函数删除向量末尾的元素:vec.pop_back();
  3. 访问和修改元素:
    • 使用下标操作符[]访问元素:int value = vec[index];
    • 使用at()函数进行安全的边界检查访问:int value = vec.at(index);
    • 使用迭代器遍历和修改元素:for (auto it = vec.begin(); it != vec.end(); ++it) { *it = newValue; }
  4. 获取向量信息:
    • 使用size()函数获取向量的大小(元素个数):int size = vec.size();
    • 使用empty()函数检查向量是否为空:if (vec.empty()) { /* 向量为空 */ }
    • 使用capacity()函数获取向量的容量(内部存储空间大小):int capacity = vec.capacity();
  5. 清空和重置向量:
    • 使用clear()函数清空向量中的所有元素:vec.clear();
    • 使用erase()函数擦除指定位置的元素或指定范围的元素:vec.erase(vec.begin() + index);vec.erase(vec.begin(), vec.begin() + count);
  6. 内存优化:
    • 使用reserve()函数预分配足够的内存以避免频繁的重新分配:vec.reserve(newCapacity);
    • 使用shrink_to_fit()函数减小向量的容量以匹配其大小:vec.shrink_to_fit();
  7. 使用算法和迭代器:
    • 使用std::algorithm库中的算法和std::vector的迭代器结合进行元素查找、排序等操作。
    • 注意在修改向量时,可能需要使用迭代器的erase()insert()函数。

这些技巧可以帮助您更好地使用和操作std::vector,使您的代码更高效、可读性更好。请根据具体的需求和场景选择适当的技巧。