c++ 基础学习笔记

override

1. 生成默认的拷贝函数

在C++中,编译器会在以下几种情况下为类生成默认的拷贝构造函数:

1. 当你没有显式地声明任何拷贝构造函数时。
2. 当你声明了拷贝构造函数但没有定义它,并且没有将其标记为 `= delete` 时(在C++11及以后的标准中)。

默认拷贝构造函数执行的是浅复制(shallow copy),即它会将源对象的数据成员逐个复制到新对象。对于基本类型数据成员,这通常意味着直接复制其值;对于指针类型的成员变量,这意味着复制指针本身而不是指针所指向的对象。如果类中有动态分配的资源或者需要深层复制(deep copy)的情况,那么程序员就需要自己定义拷贝构造函数来处理这些情况。

此外,如果一个类中包含有不能被拷贝的成员(比如一个成员变量是某个禁止拷贝的类的对象),那么编译器不会自动生成默认的拷贝构造函数。在这种情况下,你需要手动提供一个拷贝构造函数,或者决定这个类不应该支持拷贝,从而显式地删除拷贝构造函数。

从C++11开始,你可以使用`= default;`来显式要求编译器生成默认的拷贝构造函数,或者使用`= delete;`来禁止拷贝构造函数的生成。例如:

```cpp
class MyClass {
public:
    // 显式要求编译器生成默认的拷贝构造函数
    MyClass(const MyClass& other) = default;

    // 禁止拷贝构造函数
    // MyClass(const MyClass& other) = delete;
};
```

当你使用`= default;`时,即使已经定义了其他构造函数或成员函数,编译器也会为你生成一个默认的拷贝构造函数。而`= delete;`则告诉编译器不要生成拷贝构造函数,并且不允许通过拷贝构造函数创建对象。

2. private`、`protected`或`public

在C++中,类的成员(包括数据成员和成员函数)可以被声明为`private`、`protected`或`public`。这些访问说明符定义了类成员的可见性和可访问性。下面是关于如何使用这些访问说明符的一般指导原则:

`public` 成员
- **数据成员**:通常不推荐将数据成员设为`public`,因为这会破坏封装性。直接暴露数据成员会导致外部代码可以直接修改对象的状态,从而可能破坏对象的一致性。
- **成员函数**:接口部分,即那些需要向类的用户公开的方法。这些方法提供了一种与类交互的方式,而无需知道类内部的具体实现。

`protected` 成员
- **数据成员**:有时为了子类能够访问基类的数据成员,但又不想让外部直接访问时,可以将数据成员设为`protected`。
- **成员函数**:如果有一些方法是专门设计给派生类使用的,而不是最终用户的,那么这些方法应该被设置为`protected`。这样可以允许派生类访问并扩展这些功能,同时保持对外部世界的隐藏。

private` 成员
- **数据成员**:通常所有数据成员都应该被声明为`private`,以保护它们不受外界的直接修改。这是封装的一个重要方面。
- **成员函数**:任何仅用于类内部实现细节的辅助函数都应该被声明为`private`。这样做可以确保这些函数不会被类外部的代码误用,并且有助于维护清晰的接口。



class Example {
private:
    int secret;  // 数据成员通常是私有的
    void internalFunction();  // 内部使用的辅助函数也是私有的

protected:
    int protectedData;  // 只有派生类可以访问
    void protectedMethod();  // 派生类可以覆盖或使用这个方法

public:
    Example(int s) : secret(s) {}  // 构造函数,公共的
    void publicMethod();  // 公共方法,供用户调用
    int getSecret() const;  // 提供一个公共方法来获取私有数据
};

// 实现
void Example::internalFunction() {
    // ...
}

void Example::protectedMethod() {
    // ...
}

void Example::publicMethod() {
    // 可能会调用 internalFunction 或者 protectedMethod
}

int Example::getSecret() const {
    return secret;
}

以上例子展示了如何合理地组织类中的`private`、`protected`和`public`成员。记住,良好的封装习惯对于创建健壮且易于维护的软件是非常重要的。

assert()关键字

`assert` 是一个宏,在调试阶段用于验证程序中的假设条件是否正确。如果 `assert` 后面的表达式求值为假(即0),那么程序会终止,并且通常会显示一个错误消息。使用 `assert` 可以帮助开发者在开发过程中尽早地发现并修复代码中的逻辑错误。

在 C 或 C++ 中,`assert` 函数定义在 `<assert.h>` 头文件中。常见的使用方式如下:
```

```c
#include <assert.h>

int main() {
    int* ptr = NULL;
    // 假设我们在函数中期望 ptr 不应该为 NULL
    assert(ptr != NULL); // 如果 ptr 为 NULL,则断言失败,程序将停止执行
    // 其他代码...
}

在你提供的例子 `"assert(ph);"`, `ph` 指的是一个指向堆的指针。这个 `assert` 语句确保了 `ph` 在继续执行之前不是 `NULL`。这是很重要的,因为如果 `ph` 是 `NULL`,那么对其解引用或进行其他操作可能会导致未定义的行为,包括程序崩溃或者数据损坏。

在生产环境中,通常不会启用 `assert` 宏,因为它会增加运行时的开销。但是,在开发和测试阶段,`assert` 是非常有用的工具,可以用来检查程序状态是否如预期那样。

请注意,`assert` 仅用于检测编程错误,而不是处理运行时错误。对于需要在运行时处理的错误情况,应该使用错误处理机制而不是 `assert`。

智能指针

智能指针(Smart Pointers)是一种在C++中用于自动管理动态分配的对象生命周期的设计模式。它们通过封装原始指针的行为,并在对象不再需要时自动释放内存,来帮助开发者避免内存泄漏和其他常见的内存管理错误。下面是几种常见的智能指针类型及其特点:

1. **std::unique_ptr**:
   - 是一种拥有独占所有权的智能指针,表示一个对象的唯一控制权。
   - 当unique_ptr超出作用域或被显式重置时,所指向的对象会被自动删除。
   - 不允许复制,但可以通过`std::move`转移所有权。

2. **std::shared_ptr**:
   - 是一种共享所有权的智能指针,允许多个shared_ptr实例同时指向同一个对象。
   - 使用引用计数机制来跟踪有多少个智能指针指向同一块内存。
   - 当最后一个指向某对象的shared_ptr销毁或被重置时,该对象才会被删除。

3. **std::weak_ptr**:
   - 用来解决shared_ptr的循环引用问题。
   - 不增加引用计数,当对应的shared_ptr不再存在时,可以安全地从weak_ptr获取null pointer。

使用智能指针的好处在于它们提供了自动化的资源管理,减少了手动管理内存带来的风险,如忘记释放内存、释放错误的内存、多次释放同一块内存等问题。然而,智能指针并不能完全消除所有问题,例如不当使用仍然可能导致内存泄漏(比如由于循环引用),因此在使用时仍需注意设计模式的合理选择和正确的使用方法。

总结来说,智能指针确实可以在很大程度上减轻开发者手动管理内存的负担,使得内存释放变得更加自动化,但也需要根据具体情况合理选用,并遵循良好的编程实践。

是的,`auto_ptr` 是一种智能指针,但它在现代 C++ 编程实践中已经被废弃。在 C++11 中引入了更安全、更强大的智能指针类型,如 `unique_ptr` 和 `shared_ptr`,它们提供了更好的资源管理和更清晰的所有权语义。

`auto_ptr` 的主要特点是它会自动删除它所持有的对象,当 `auto_ptr` 对象本身超出作用域或者被另一个 `auto_ptr` 赋值或转移拥有权时。然而,`auto_ptr` 存在一些设计上的缺陷,比如不支持自增自减操作(这在某些情况下可能会用到),以及所有权转移时的行为可能会导致一些意外的问题(如 self-aliasing 问题)。

由于这些原因,在 C++17 中,`auto_ptr` 被从标准库中移除了,并且不再推荐使用。如果你正在开发新的 C++ 代码,应该使用 `unique_ptr` 或 `shared_ptr`,它们不仅功能更强大,而且避免了 `auto_ptr` 的许多陷阱。

双重间接引用

在编程语言中,“双重间接引用”指的是通过两个层级的指针来访问数据的一种方式。为了更好地理解这个概念,我们先从基础开始解释:

1. **直接引用**:当我们有一个变量并直接使用它的名字时,这就是直接引用。例如:
   ```c++
   int x = 10;
   // 直接使用x
   ```

2. **单级间接引用(普通指针)**:当我们有一个指向变量的指针时,可以通过解引用操作符`*`来访问该变量。例如:
   ```c++
   int x = 10;
   int *p = &x;  // p 指向 x
   // 间接使用 x: *p
   ```

3. **双重间接引用(二级指针)**:当我们有一个指针,它指向另一个指针,而那个指针又指向实际的数据时,这就构成了双重间接引用。例如:
   ```c++
   int x = 10;
   int *p = &x;  // p 指向 x
   int **pp = &p;  // pp 指向 p
   // 双重间接使用 x: **pp
   ```

在你的例子 `void InitSqList(SqList * &L);` 中,`*&L` 实际上是有些误导性的写法。在C++中,如果我们想修改一个指针变量本身,我们可以使用引用到指针的参数。如果`L`是一个指向`SqList`类型的指针,那么`*&L`实际上会取消一次引用,获取`L`所指向的指针,这并不是典型的双重间接引用的形式。

如果你的意思是想修改由`L`引用的那个指针变量,那么正确的写法应该是这样的:
```c++
void InitSqList(SqList *& L);
```

在这个例子中,`L`是一个指向`SqList`类型的指针的引用。这意味着函数可以修改`L`所引用的指针变量,让它指向一个新的`SqList`对象。这通常用于“输出参数”的场景,其中函数负责创建或改变外部可见的指针。

如果你有更多具体的需求或者疑问,请提供更多的上下文或详细描述。

i++ 打印顺序

 int i =10;

    cout<< i++ <<" "<< i++<<i  << endl; //10  11 12  从左到右边执行

override关键字

在C++中,`override`关键字用于声明一个成员函数是覆盖(override)其基类中的虚函数。在您的代码示例中,

```cpp
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
```

这一行代码表示`SetupPlayerInputComponent`函数是一个虚函数,并且它覆盖了其基类中的同名虚函数。

### 使用`override`的目的

1. **明确意图**:使用`override`关键字可以清楚地表明您打算覆盖基类中的虚函数。这对于代码的可读性和维护性非常重要。

2. **编译时检查**:如果基类中没有对应的虚函数可以被覆盖,编译器将会报错。这有助于捕捉错误,防止无意中覆盖错误的函数或忘记声明虚函数。

3. **类型安全**:确保覆盖的函数签名与基类中的虚函数完全一致,包括返回类型、参数列表和抛出的异常。

### 示例

假设有一个基类`Actor`和一个派生类`Player`,其中`Actor`定义了一个虚函数`SetupPlayerInputComponent`:```cpp

 

class Actor {
public:
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent);
};

class Player : public Actor {
public:
    // 使用override关键字
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override {
        // 具体实现...
    }
};
```

在这个例子中,`Player`类覆盖了`Actor`类中的`SetupPlayerInputComponent`函数。使用`override`关键字可以确保`Player`类中的实现确实是覆盖了基类中的虚函数。

### 注意事项

- 如果派生类中的函数签名与基类中的虚函数不匹配,编译器将产生错误。
- `override`只能用于覆盖虚函数,如果基类中的函数不是虚函数,则无法使用`override`关键字。
- 在C++11之前,没有`override`关键字,开发者需要手动检查函数签名是否正确。

总之,`override`关键字是为了提高代码的安全性和可读性,确保派生类中的函数确实覆盖了基类中的虚函数。在游戏开发中,特别是在使用继承和多态的情况下,`override`是非常有用的。

引用&

在C++中,当你声明一个引用(`&`)时,实际上并没有为这个引用开辟新的内存空间。引用只是一个别名,它必须在创建的时候就初始化,并且之后不能改变其绑定的对象。当你说`const`修饰一个引用时,比如`int& const ref = variable;` 或者更常见的形式`const int& ref = variable;`,这种情况下,引用本身不是`const`的,而是引用所指向的对象是`const`的,也就是说,你不能通过这个引用去修改它所引用的对象。

这里有几个点需要注意:

- 引用必须在定义时初始化,并且不能重新绑定到另一个对象上。
- 如果引用前面有`const`关键字(如`int& const ref`),这意味着引用本身不可更改;但是这种写法并不常见,通常我们会把`const`放在前面,如`const int& ref`,这表示通过引用所访问的数据是只读的。
- `const`修饰引用通常用于函数参数中,这样可以保证函数内部不会修改传入的参数值,这对于常量正确性和提高代码可读性都有帮助。

总结来说,无论是否使用`const`修饰引用,都不会为引用开辟新的内存空间,`const`只是用来限制通过该引用进行修改的可能性。

友元函数

在C++中,友元函数(friend function)是一种特殊的函数,它能够访问类的私有(private)和保护(protected)成员,尽管它不是该类的成员函数。友元函数的概念是C++封装原则的一个例外,它提供了更灵活的方式来实现某些功能,尤其是在需要让非成员函数访问类内部数据的时候。

友元函数的作用包括但不限于:

  1. 提高访问控制的灵活性:有时候,两个类之间有着密切的关系,一个类的方法可能需要访问另一个类的私有成员。在这种情况下,可以将一个类的某个方法声明为另一个类的友元函数。

  2. 操作符重载:对于某些运算符(如<<>>),它们通常用于输入输出流,为了使这些运算符重载更加自然地工作,通常会定义它们为友元函数。这是因为运算符通常需要修改对象的状态,同时又不是对象本身的方法调用。

  3. 辅助函数:当一个函数需要帮助一个类完成某些任务,并且这个任务涉及到类的内部实现细节时,可以将其声明为友元函数。这允许函数访问必要的私有数据,而无需通过公共接口。

  4. 性能优化:在某些情况下,将函数声明为类的成员可能会导致额外的开销(例如,this指针的使用)。如果函数不需要被继承或者不希望增加这种开销,那么可以考虑使用友元函数。

class MyClass {
public:
    // 公共成员
private:
    // 私有成员
    friend void myFriendFunction(MyClass &obj);  // 声明myFriendFunction为友元函数
};

1.内存分配 new int

c语言才是用malloc()函数分配内存,c++是用new 来分配内存,开辟内存

int *array;
6    int n = 10;  // 定义数组的长度为10
7
8    // 动态分配内存
9    array = (int*) malloc(n * sizeof(int));
10    if (array == NULL) {
11        printf("内存分配失败!\n");
12        return 1;  // 返回错误码
13    }

2. 内存泄漏

#include <iostream>
#include <cstdlib>

int allocated = 0; // 记录分配次数
int deallocated = 0; // 记录释放次数

// 自定义 new 操作符
void* operator new(size_t size) {
    allocated++;
    std::cout << "Allocated: " << allocated << std::endl;
    return malloc(size);
}

// 自定义 delete 操作符
void operator delete(void* ptr) noexcept {
    free(ptr);
    deallocated++;
    std::cout << "Deallocated: " << deallocated << std::endl;
}

class SimpleClass {
public:
    SimpleClass() {
        std::cout << "SimpleClass constructed" << std::endl;
    }
    ~SimpleClass() {
        std::cout << "SimpleClass destructed" << std::endl;
    }
};

int main() {
    SimpleClass* obj = new SimpleClass(); // 分配内存
    delete obj; // 释放内存

    // 如果没有内存泄漏,则这两个数应该相等
    if (allocated == deallocated) {
        std::cout << "No memory leaks detected." << std::endl;
    } else {
        std::cout << "Memory leaks detected!" << std::endl;
    }

    return 0;
}

这个示例中,我们重载了全局的 newdelete 运算符来记录每次分配和释放内存的情况。当 main 函数结束时,我们可以检查分配和释放的次数是否相同来判断是否存在内存泄漏。

accumulate

是C++标准库中的一个函数,位于<numeric>头文件中。它主要用于对容器(如vector, list, deque等)中的元素进行累加求和,也可以用于其他可迭代范围。

accumulate 函数的基本语法如下:

#include <numeric>
#include <vector>

// ... 初始化容器

auto result = std::accumulate(container.begin(), container.end(), initial_value);

其中:

  • container.begin() 和 container.end() 分别是容器的开始和结束迭代器,指明了要累加的元素范围。
  • initial_value 是累加的初始值,通常是一个数值类型(如intdouble等)。累加的结果将从这个值开始计算。

accumulate 函数将从initial_value开始,依次将容器中每个元素与累积的和相加,直到遍历完所有元素,最后返回总和。

此外,accumulate 还支持使用一个可选的二元操作符(即用户定义的累加逻辑),这样就可以实现比简单的求和更复杂的操作。例如:

auto result = std::accumulate(container.begin(), container.end(), 0, std::multiplies<int>());

这里是一个具体的例子,展示了如何使用accumulate函数来计算一个整数向量的总和:

#include <iostream>
#include <vector>
#include <numeric>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    int sum = std::accumulate(numbers.begin(), numbers.end(), 0);
    std::cout << "Sum: " << sum << std::endl;
    return 0;
}

这段代码将输出数字1到5的和,即15。

2.异常类

继承

用&err表示引用, 不用创建对象,导致内存空间的浪费,throw会创建一个out_of_range的对象开辟内存空间

3.析构函数

在C++中,析构函数是一种特殊的成员函数,用于在对象生命周期结束时执行清理操作,比如释放对象占用的资源。析构函数的名称与类名相同,但在前面加上波浪号 `~`。每个类最多只能有一个析构函数,且它不能被显式调用,也不能有返回类型,也不能带有参数。

### 析构函数的基本语法

```cpp
class ClassName {
public:
    // 构造函数
    ClassName() { /* 初始化资源 */ }

    // 析构函数
    ~ClassName() { /* 清理资源 */ }
};

```

### 析构函数的调用时机

析构函数会在以下几种情况被自动调用:

1. 当一个对象到达其作用域的末尾时。
2. 当一个动态分配的对象(使用`new`关键字创建)被`delete`时。
3. 当一个对象所在的函数返回时。
4. 当程序正常结束时,全局对象的析构函数会被调用。

### 析构函数的特点

1. **自动调用**:析构函数由编译器自动调用,无需程序员手动调用。
2. **无参数**:析构函数不允许有任何参数。
3. **无返回类型**:析构函数没有返回类型,即使是`void`也不行。
4. **单个析构函数**:一个类只能有一个析构函数。
5. **虚析构函数**:如果一个类是基类,并且可能有派生类的对象通过基类指针或引用销毁,那么应该将基类的析构函数声明为虚函数。这确保了正确的析构函数被调用。

### 虚析构函数的示例```cpp

class Base {
public:
    virtual ~Base() { /* 清理Base类的资源 */ }
};

class Derived : public Base {
public:
    ~Derived() override { /* 清理Derived类的资源 */ }
};

int main() {
    Base* basePtr = new Derived();
    delete basePtr; // 正确调用Derived的析构函数,然后是Base的析构函数
    return 0;
}


```

在这个例子中,如果`Base`的析构函数不是虚函数,那么`delete basePtr;`只会调用`Base`的析构函数,导致`Derived`的资源没有被正确释放。而通过声明`Base`的析构函数为虚函数,可以确保`Derived`的析构函数首先被调用,然后才是`Base`的析构函数。

析构函数是C++中资源管理的关键组成部分,特别是在使用智能指针(如`std::unique_ptr`, `std::shared_ptr`)不那么普遍的时代。现代C++鼓励使用RAII(Resource Acquisition Is Initialization)原则和智能指针来自动管理资源,但这并不减少理解析构函数重要性。

4. 结构体和共用体的区别

在C和C++中,结构体(struct)和共用体(union)都是用于组合不同类型的数据形成单一实体的数据类型,但它们在存储和访问数据方面有着根本的不同。以下是结构体和共用体的主要区别:

结构体(Struct)

1. **存储方式**:结构体中的成员在内存中是连续存储的,每个成员都从一个新的偏移量开始,通常是按照成员的自然对齐方式来安排的。为了数据对齐,编译器可能会在某些成员之间添加填充字节,以确保每个成员都能在适当的边界上对齐。

2. **内存占用**:结构体占用的总内存等于所有成员占用的内存之和加上任何填充字节。例如,如果一个结构体包含一个`char`和一个`int`,即使`char`只需要1字节,而`int`需要4字节,结构体也可能占用8字节的内存,以便`int`能在4字节边界上对齐。

3. **访问成员**:你可以独立地访问结构体中的每个成员,每个成员都有其独立的内存空间。

 共用体(Union)

1. **存储方式**:共用体中的所有成员共享同一段内存区域。这意味着,无论何时你修改一个成员,都可能会影响到其它成员的值,因为它们都在同一块内存中。

2. **内存占用**:共用体占用的内存大小等于其最大成员的大小,加上任何必要的对齐填充。这是因为所有成员都共享同一块内存,所以只需足够大的空间来容纳最大的那个成员即可。

3. **访问成员**:你不能同时访问共用体中的多个成员,因为它们共享同一块内存。每次只有一个成员的值是有效的,当你写入一个成员时,其他成员的值将被覆盖。

### 示例

假设我们有以下定义:

```cpp
struct Point {
    int x;
    int y;
};

union Data {
    int i;
    float f;
    char c;
};
```

对于`Point`结构体,`x`和`y`都有各自独立的内存空间,总共占用8字节(假设`int`和`float`都是4字节)。而`Data`共用体中,`i`、`f`和`c`共享同一块内存,所以`Data`共用体的大小将是4字节,这是`int`和`float`的最大大小,加上可能的对齐填充。

总的来说,结构体用于组织不同类型的成员,而共用体用于在有限的内存空间中存储不同类型的值,通常用于节省内存或实现特定的硬件交互。

5. 指针函数和函数指针

在C和C++中,“指针函数”和“函数指针”这两个术语经常被提及,但它们实际上指的是不同的概念:

函数指针

函数指针是指向函数的指针。它可以存储函数的地址,并通过这个指针来调用函数。函数指针允许你在运行时动态地选择调用哪个函数,这对于实现回调函数、策略模式等很有用。

#### 函数指针的声明和使用

函数指针的声明需要指定函数的返回类型和参数类型。例如,以下是一个返回`int`类型并接受两个`int`类型参数的函数指针的声明:

```cpp
int (*func_ptr)(int, int);
```

接下来,你可以将一个具有匹配签名的函数的地址赋给这个指针:

```cpp
int add(int a, int b) {
    return a + b;
}

int main() {
    func_ptr = &add; // 或者简写为 func_ptr = add;
    int result = func_ptr(3, 4); // 调用 add 函数
    return 0;
}
```

指针函数

指针函数是一个返回指针的函数,也就是说,这个函数的返回类型是一个指针类型。这样的函数通常用于动态分配内存并返回指向这块内存的指针,或者返回指向数组、字符串等的指针。

#### 指针函数的声明和使用

例如,以下是一个返回指向`int`类型指针的函数:

```cpp
int* getPointer() {
    int* p = new int(10);
    return p;
}
```

在`main`函数中,你可以调用`getPointer`并使用返回的指针:

```cpp
int main() {
    int* ptr = getPointer();
    std::cout << *ptr << std::endl; // 输出 10
    delete ptr; // 不要忘记释放内存
    return 0;
}
```

### 区别总结

- **函数指针**:是一个指向函数的指针,可以用来存储函数的地址并调用函数。
- **指针函数**:是一个返回指针的函数,通常返回动态分配的内存地址或其他数据结构的指针。

两者的关键区别在于它们的用途和返回类型。函数指针用于存储和调用函数,而指针函数返回一个指针,这个指针可以指向动态分配的内存或其他数据结构。理解这两者的区别有助于更灵活地使用C/C++进行编程。

6.线程和进程

//
// Created by 19342 on 2024/8/29.
//


#include <thread>

#include "iostream"
#include <string>
using namespace std;

void print_hello() {

    cout << "Hello World!" << endl;
}

void print_hello2(string msg) {

    cout <<msg<< endl;
    return;
}
int main() {

    // thread t(print_hello);
    // t.join();
    thread t2(print_hello2,"hello thread");

    // t2.join();

    t2.detach();  //当主线程结束时,子线程依旧可以在后台运行
    return 0;

}

detach()是指 main函数执行完了,print_hello2()还在运行,还在打印,但是程序没有显示打印的东西,程序也不会报错,一般都是用join函数偏多

join :当子线程全部执行完后,主线程才会执行

//
// Created by 19342 on 2024/8/29.
//


#include <thread>

#include "iostream"
#include <string>
using namespace std;

void print_hello() {

    cout << "Hello World!" << endl;
}

void print_hello2(string msg) {

    cout <<msg<< endl;
    return;
}

void print_hello3() {
    for (int i = 0; i < 10000; ++i)
        cout << i << endl;
    cout << endl;

}
int main() {

    // thread t(print_hello);
    // t.join();
    // thread t2(print_hello2,"hello thread");
    thread t3(print_hello3);

    // t2.join();

   bool isJoin =  t3.joinable(); //判断是否能调用

    if(isJoin) {   //严谨一些,防止程序报错
        t3.join();
    }
    cout << "over"<<endl;

    // t2.detach();  //当主线程结束时,子线程依旧可以在后台运行
    return 0;

}

6.2  线程函数参数定义错误

6. C++14新特性

C++14是C++标准的一个修订版,它在C++11的基础上增加了若干新特性和改进,旨在提高语言的易用性和效率。以下是C++14中的一些主要新特性:

  1. Lambda表达式的改进

    • Lambda 捕获列表中的变量初始化:可以在捕获列表中初始化变量,只要它们在lambda表达式中被定义时就赋值。
    • 通用lambda:lambda表达式可以接受任何类型的参数,只要类型能够满足lambda内部的操作。
  2. constexpr的增强

    • constexpr函数中的控制流:可以在constexpr函数中使用更复杂的控制流结构,如循环和switch语句。
    • constexpr if:虽然constexpr if直到C++17才正式引入,但在C++14中,constexpr函数和变量的使用得到了扩展,使得更多操作可以在编译期执行。
  3. 类型推导

    • 返回类型推导:可以在函数定义中使用auto关键字来推导返回类型,这在lambda表达式中尤其有用。
  4. 数字字面量

    • 二进制字面量:可以使用前缀0b0B来表示二进制数。
    • 数字分隔符:可以使用下划线_作为数字中的分隔符,提高读取大数值的易读性。
  5. Deprecation(弃用)属性

    • 可以使用[[deprecated]]属性标记函数、类或变量,告知使用者该元素即将被废弃,编译器会发出警告。
  6. 放宽new/delete的限制

    • 允许省略newdelete操作符后的类型,如果类型可以从上下文中推导出来。
  7. 变量模板

    • 引入了模板变量,使得可以创建模板化的变量,这在元编程中非常有用。
  8. 其他改进

    • 简化类型别名:可以使用using声明类型别名,更简洁明了。
    • 默认构造函数的明确性:可以通过= default显式声明默认构造函数、析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符和移动赋值运算符。
    • 右值引用的改进:C++14允许在某些情况下使用右值引用来改进性能和减少代码冗余。

这些特性共同使得C++14更加现代化,提供了更多的工具来帮助程序员写出更高效、更安全、更易于维护的代码。如果你有特定的C++14特性想要深入了解,可以告诉我,我可以提供更详细的解释。

在C++中,右值引用主要用于实现移动语义和完美转发。C++14对右值引用的改进主要体现在两个方面:对模板参数的更灵活使用以及对临时对象的更有效处理。下面我将通过代码示例来说明这两个方面的改进。

1. 完美转发(Perfect Forwarding)

C++11已经支持完美转发,但是C++14在此基础上进行了优化,使得我们可以更简洁地实现转发。例如,考虑一个通用的包装函数,它需要将接收到的所有参数原封不动地转发给另一个函数:

template<typename... Args>
void wrapper(Args&&... args) {
    some_function(std::forward<Args>(args)...);
}

// C++14之前,需要在函数调用中重复参数列表
template<typename... Args>
void old_wrapper(Args&&... args) {
    some_function((Args &&)args...); // 需要显式转换每个参数
}

// C++14之后,可以使用更简洁的语法
template<typename... Args>
void new_wrapper(Args&&... args) {
    some_function(std::forward(args)...); // 不再需要显式转换每个参数
}

在C++14中,你可以直接在函数调用的参数列表中使用std::forward,而不需要为每个参数添加&&和括号,这减少了代码冗余并且提高了可读性。

2. 更有效的临时对象处理

C++14允许编译器更自由地决定何时使用临时对象,这可以导致更好的性能。例如,当一个函数返回一个右值引用时,编译器可以自动将其转换为一个临时对象,从而避免不必要的复制。这是一个例子:

struct MyType {
    MyType(const MyType&) { /* 复制构造函数 */ }
    MyType(MyType&&) { /* 移动构造函数 */ }
};

MyType createMyType() {
    return MyType(); // 返回一个临时对象
}

int main() {
    MyType mt = createMyType(); // 在C++14中,这里会使用移动构造函数,而非复制构造函数
    return 0;
}

在上述例子中,createMyType函数返回一个临时对象。在C++14中,编译器会自动选择使用移动构造函数来初始化mt,即使函数的返回类型没有明确指出是右值引用。这是因为在C++14中,函数返回类型可以隐式转换为右值引用,从而使得移动语义能够被更广泛地应用。

这些改进使C++14的代码更简洁、更高效,同时保持了C++的强大功能和灵活性。

黑马笔记

1. 引用&

2.引用传递

3.值传递和地址传递

指针传递,&x,&y,取地址符号

总结

7.网络编程

下面是使用 C++ 编写的简单 UDP 服务器和客户端示例。这个例子中,服务器接收客户端发送的消息,并将其转换为大写后返回给客户端。

#include <iostream>
#include <string>
#include <cstring>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>

#define PORT 10000
#define BUFFER_SIZE 1024

int main() {
    int server_socket;
    struct sockaddr_in server_addr;

    // 创建一个UDP socket
    if ((server_socket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("Socket creation failed");
        return -1;
    }

    // 设置服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // 绑定socket
    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        return -1;
    }

    std::cout << "Server is running on port " << PORT << std::endl;

    char buffer[BUFFER_SIZE];
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    while (true) {
        // 清空缓冲区
        memset(buffer, 0, sizeof(buffer));

        // 接收数据
        ssize_t len = recvfrom(server_socket, buffer, BUFFER_SIZE, 0,
                               (struct sockaddr*)&client_addr, &client_len);
        if (len < 0) {
            perror("Error receiving data");
            continue;
        }

        // 处理数据
        std::string message(buffer);
        std::transform(message.begin(), message.end(), message.begin(), ::toupper);

        // 发送响应
        sendto(server_socket, message.c_str(), message.length(), 0,
               (struct sockaddr*)&client_addr, client_len);

        std::cout << "Received: " << buffer << ", Sent: " << message << std::endl;
    }

    close(server_socket);
    return 0;
}

UDP 客户端代码

#include <iostream>
#include <string>
#include <cstring>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>

#define SERVER_IP "127.0.0.1"
#define PORT 10000
#define BUFFER_SIZE 1024

int main() {
    int client_socket;
    struct sockaddr_in server_addr;

    // 创建一个UDP socket
    if ((client_socket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("Socket creation failed");
        return -1;
    }

    // 设置服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    char buffer[BUFFER_SIZE];
    std::string message;

    while (true) {
        std::cout << "Enter a message: ";
        std::cin >> message;

        // 发送数据
        if (sendto(client_socket, message.c_str(), message.length(), 0,
                   (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
            perror("Send failed");
            break;
        }

        // 清空缓冲区
        memset(buffer, 0, sizeof(buffer));

        // 接收响应
        ssize_t len = recvfrom(client_socket, buffer, BUFFER_SIZE, 0, NULL, NULL);
        if (len < 0) {
            perror("Receive failed");
            break;
        }

        std::cout << "Server response: " << buffer << std::endl;
    }

    close(client_socket);
    return 0;
}

编译和运行

  1. 编译服务器:

     bash 

    浅色版本

    1g++ -std=c++11 -o udp_server udp_server.cpp -lsocket
  2. 编译客户端:

     bash 

    浅色版本

    1g++ -std=c++11 -o udp_client udp_client.cpp -lsocket
  3. 运行服务器:

     bash 

    浅色版本

    1./udp_server
  4. 运行客户端:

     bash 

    浅色版本

    1./udp_client

注意:在运行客户端之前,请确保服务器已经在运行。此外,服务器和客户端应该在同一台计算机上运行,或者客户端应该知道服务器的正确IP地址。

8. 模板元编程

模板元编程是C++中一种利用编译器的能力在编译时执行计算的技术。它通常涉及使用模板和类型trait来创建复杂的类型和函数,这些类型和函数可以在编译时被解析和优化。下面我会给出一个简单的例子来说明如何使用模板元编程来实现一个加法运算。

假设你想创建一个模板类来实现两个整数的加法操作,这可以在运行时完成,但也可以在编译时完成。这里是一个简单的例子:

template<int A, int B>
struct Add {
    static const int value = A + B;
};

int main() {
    // 使用模板元编程来计算 5 + 10
    static_assert(Add<5, 10>::value == 15, "Addition failed");

    return 0;
}

在这个例子中,Add 是一个模板结构体,它接受两个整数类型的模板参数 ABAdd 结构体中的 value 成员是一个常量表达式,它计算 A + B 的结果,并且这个计算是在编译时完成的。

static_assert 是用来验证 Add<5, 10>::value 是否等于 15。如果表达式的结果为假,则编译失败,并显示错误消息 "Addition failed"

模板元编程可以变得非常复杂,例如递归模板元编程。这里是一个使用递归模板元编程实现阶乘的例子:

// 阶乘函数的递归定义
template<int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

// 递归的基本情况
template<>
struct Factorial<0> {
    static const int value = 1;
};

int main() {
    // 计算 5 的阶乘
    static_assert(Factorial<5>::value == 120, "Factorial failed");

    return 0;
}

在这个例子中,Factorial 是一个递归模板,它有一个特殊情况 Factorial<0> 作为递归的基本情况,其值为 1。对于所有其他正整数 N,阶乘的值通过 N * Factorial<N - 1>::value 计算得出。

模板元编程可以用来实现许多有用的功能,比如静态数组、元组、类型列表等。如果你想了解更多关于模板元编程的内容,可以查阅C++标准库中的 <type_traits> 头文件,以及相关的书籍和在线资源。

错误点

解决方案

  1. 移除 (...):

    • 将 #define GENERATED_BODY(...) 更改为 #define GENERATED_BODY(),去掉括号。
    • 这样,GENERATED_BODY 就会被正确地展开为一个函数定义,而不是一个函数调用

const关键字

for (const auto& pair : dataMap)  

在这段代码中,const auto& 是 C++ 中的一个常见用法,用于在遍历容器(如 std::mapstd::unordered_map)时声明一个引用到容器元素的常量迭代器。具体来说:

  • auto: 这个关键字用于让编译器自动推断变量的类型。在这里,编译器会根据 dataMap 的类型推断出 pair 的类型为 std::pair<const KeyType, ValueType>,其中 KeyType 和 ValueType 是 dataMap 定义时使用的键值对类型。
  • &: 引用符,表示 pair 是一个对容器元素的引用,而不是复制元素本身。这样可以避免在每次迭代时复制元素,提高性能。
  • const: 表示这个引用是只读的,你不能通过这个引用修改容器中的元素。这对于只读操作是非常有用的,因为它可以保护容器内的数据不被意外修改。

这段代码的作用是打印 dataMap 在删除之前的状态。具体步骤如下:

  1. 输出提示信息 "Before deletion:"
  2. 遍历 dataMap 中的每一个键值对 (key-value pair)。
  3. 对于每个键值对,输出键 (pair.first) 和值 (pair.second)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值