C++基石:掌握高效编程的艺术

news2024/11/27 7:21:32

C++ 关于命名空间:namespace

在这里插入图片描述

上述文档详细介绍了C++标准库(Standard C++ Library)的一些关键约定,这些约定不仅帮助开发者理解如何正确使用库中的功能,也明确了实现者在设计库时的灵活性和限制。下面是对文档中提到的几个要点的详细解释:

1. 名称空间和宏

  • 名称空间:C++标准库中的所有名称(除了宏)都被声明在std名称空间内。这意味着当你包含了一个标准库头文件,如<iostream>,你必须通过std::cin这样的完全限定名来访问cin。要避免每次使用std下的名称都加上std::前缀,可以在所有包含标准库头文件的#include指令后立即写上using namespace std;

  • :宏不受任何命名空间的约束,因此它们的行为独立于std名称空间之外。

2. C库与C++库的区别

  • 当你包含C库头文件,如<cstdlib>,你调用的std::abort()拥有C++风格的名称空间限定;然而,如果你包含的是<stdlib.h>(C风格的头文件),你则直接调用abort()

3. 实现细节

  • 函数链接属性:C库中的函数可以具有C++或C链接属性。为了保证正确性,应当通过包含相应的C库头文件来声明这些函数,而不是内联声明。

  • 成员函数签名:库类的成员函数可能有额外的未列出的函数签名。你可以确信按文档描述调用这些函数会得到预期的行为,但是获取成员函数的地址可能不会得到期望的类型。

  • 基类和派生关系:库中的类可能有未文档化的(非虚)基类。这意味着一个类实际上可能通过其他未记录的类从另一个类派生。

  • 类型同义词:定义为某种整数类型同义词的类型,可能与几种不同的整数类型相同。

  • 异常抛出:没有异常规格说明的库函数可能抛出任意异常,除非其定义明确限制了这种可能性。

4. 可靠的约定

  • 无掩蔽宏:标准C库不使用掩蔽宏。只有具体的函数签名被保留,而非函数名称本身。

  • 外部函数签名:类外的库函数不会有额外的未文档化函数签名,你可以可靠地获取其地址。

  • 虚函数和基类:被描述为虚的基类和成员函数确实为虚,而描述为非虚的确实为非虚。

  • 类型差异性:由C++标准库定义的两种类型,除非文档明确指出它们相同,否则总是不同的。

  • 异常规格说明:库提供的函数(包括可替换函数的默认版本)最多只能抛出异常规格说明中列出的异常。

通过理解这些约定,开发者能够更加有效地利用C++标准库,同时也为库的实现者提供了设计上的指导和自由度。

在C++编程中,关于是否使用using namespace std;一直存在争议,主要原因是它涉及到命名空间的使用以及代码的可读性和安全性。

不使用using namespace std;的优点:

  1. 避免命名冲突std命名空间包含了C++标准库的所有元素。不使用using namespace std;可以防止不经意间覆盖标准库中的名称,尤其是当你的代码中也使用了类似的名字时。例如,如果你有一个名为string的局部变量,它可能会与std::string冲突,导致编译错误或运行时错误。

  2. 增强代码可读性:通过在每个使用标准库元素的地方显式添加std::前缀,代码的意图变得更加清晰。读者可以很容易地分辨哪些是标准库的元素,哪些是你自己定义的元素。

  3. 鼓励良好的编程习惯:显式使用std::前缀可以促使程序员更仔细地思考他们正在使用的库元素,从而促进对标准库的深入理解和正确使用。

  4. 便于调试和维护:当代码中出现错误时,显式的命名空间使用可以帮助更快地定位问题源,特别是当涉及到标准库的使用时。

使用using namespace std;的优点:

  1. 代码简洁:不需要在每个标准库元素前加上std::前缀,可以使代码看起来更简洁,减少敲击键盘的次数。

  2. 快速原型开发:在编写快速原型或小型项目时,使用using namespace std;可以节省时间,使代码编写过程更加流畅。

结论:

在专业和大型项目中,通常推荐不使用using namespace std;,以避免潜在的命名冲突,增强代码的可读性和维护性。而在编写教学代码、快速原型或个人小项目时,使用using namespace std;可以接受,因为它使得代码更为简洁。

无论选择哪种方式,重要的是保持代码的一致性和清晰性,确保团队中的每个成员都能容易理解和维护代码。在团队项目中,最好能达成一致的编码规范,以避免混淆和潜在的错误。

因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。

auto

在C++中,auto关键字的引入极大地简化了类型声明,尤其是在类型复杂或者冗长的情况下。auto关键字最早是在C++11中正式加入的,随后在C++14和后续版本中得到了进一步的增强。以下是auto在C++中的几种常见用法:

1. 变量声明

auto可以用于变量声明,此时编译器会根据初始化表达式的类型自动推断变量的类型:

Cpp深色版本

1auto myVariable = 42;  // myVariable的类型为int
2auto myString = "Hello"; // myString的类型为const char*
3auto myVector = std::vector<int>{1, 2, 3}; // 类型为std::vector<int>

2. 函数返回类型推导

在C++14及以后的版本中,auto可以用于函数的返回类型声明,只要函数体中有且只有一个返回语句,编译器就能推断出返回类型:

Cpp深色版本

1auto add(int a, int b) -> int { return a + b; } // C++11
2auto add(int a, int b) { return a + b; }        // C++14

3. 增强的for循环

在for循环中使用auto可以避免每次循环都需要写出容器元素的类型:

Cpp深色版本

1std::vector<int> numbers = {1, 2, 3, 4};
2for (auto &num : numbers) {
3    num *= 2; // 自动推断num为int&
4}

4. 指针和引用

auto可以与指针和引用一起使用,类型推导会考虑到这些修饰符:

Cpp深色版本

1int data[] = {1, 2, 3};
2auto *ptr = data; // ptr的类型为int*
3
4int &ref = data[0]; // ref的类型为int&

5. 与const结合

当与const一起使用时,auto可以正确推断出变量的类型包括const修饰符:

Cpp深色版本

1const int val = 10;
2auto constVal = val; // constVal的类型为const int

6. lambda表达式

在lambda表达式中,auto用于捕获列表可以自动推导捕获变量的类型:

Cpp深色版本

1int x = 42;
2auto lambda = [x]() { return x; }; // x的类型被正确推导

注意事项

  • auto必须在声明时初始化,以便编译器能够推断其类型。
  • 在模板和某些上下文中,auto的使用可能受到限制,因为编译器可能无法在编译时推断类型。
  • 尽管auto使得代码更简洁,但在某些情况下,显式指定类型可以提高代码的可读性和易于理解。

使用auto时,要权衡代码的简洁性和可读性,确保在适当的情况下使用它,以提高代码的质量和维护性。

auto初始化

在C++中,auto关键字用于声明变量时,必须伴随初始化表达式,这样编译器才能根据初始化表达式推断出变量的实际类型。初始化可以是多种方式,包括赋值、构造函数调用、复合赋值运算符等。

以下是使用auto初始化变量的几种常见方式:

  1. 简单赋值初始化

    auto x = 10; // x 的类型为 int
    
  2. 使用花括号初始化

    auto y = {1, 2, 3}; // y 的类型为 std::initializer_list<int>
    

    如果初始化列表被用于一个支持列表初始化的容器,比如std::vectorstd::array,那么auto将推断出正确的容器类型:

    auto vec = {1, 2, 3}; // vec 的类型为 std::vector<int>
    
  3. 复合赋值初始化

    auto z = 5 * 5; // z 的类型为 int
    
  4. 使用构造函数初始化
    对于类类型,auto可以基于构造函数参数推断类型:

    struct Point {
        int x, y;
        Point(int _x, int _y) : x(_x), y(_y) {}
    };
    
    auto p = Point(10, 20); // p 的类型为 Point
    
  5. 使用C++17的iffor语句中的声明
    在C++17中,可以在iffor语句中声明auto变量:

    auto findValue = 42;
    for (auto i = 0; i < 10 && i != findValue; ++i) {
        // ...
    }
    

    或者在if语句中:

    auto result = someFunction();
    if (auto value = result; value > 0) {
        // ...
    }
    
  6. 与引用和指针结合使用
    当与引用或指针一起使用时,auto可以正确推断出基础类型:

    int data[] = {1, 2, 3};
    auto *ptr = data; // ptr 的类型为 int*
    auto &ref = data[0]; // ref 的类型为 int&
    
  7. 在模板中使用
    在模板中使用auto可以推断出模板参数的类型:

    template<typename T>
    auto sum(T a, T b) {
        return a + b;
    }
    

需要注意的是,auto关键字不能用于声明未初始化的变量,这是因为编译器需要初始化表达式来推断类型。此外,尽管auto可以带来代码的简洁性和可读性,过度使用它可能会降低代码的可读性,特别是当变量的类型对于理解代码逻辑很重要时。因此,在使用auto时应保持适度,特别是在大型项目中,确保代码的清晰和易维护性。

C++内联函数

内联函数(Inline Function)是C++中一种特殊类型的函数,它的主要目的是提高程序的执行效率,减少函数调用的开销。内联函数在编译时会被编译器展开成一系列的机器码,就像宏定义一样被替换,但与宏定义不同的是,内联函数提供了类型安全和编译时检查。

内联函数的声明

内联函数通过在函数声明前添加inline关键字来定义。例如:

inline int square(int x) {
    return x * x;
}

内联函数的工作原理

当编译器遇到内联函数调用时,它会尝试将函数体的代码直接插入到调用点,从而避免了普通函数调用所带来的栈帧操作、参数传递和返回地址保存等开销。这种替换称为内联(Inlining)。

详细的工作原理:

内联函数(Inlining)的工作原理涉及到编译器如何优化函数调用以减少运行时的开销。下面我们将深入探讨这一过程的细节:

1. 函数调用的常规流程

在没有内联的情况下,函数调用通常涉及以下步骤:

  • 保存当前状态:包括保存CPU寄存器的值和当前指令指针(返回地址),以便在函数调用完成后能恢复执行。
  • 参数传递:将参数压入栈中或使用寄存器传递给函数。
  • 跳转到函数入口:通过改变指令指针指向函数的第一条指令。
  • 执行函数体:在函数内部执行指令。
  • 清理栈帧:函数结束时,可能需要清除之前压入栈中的参数。
  • 恢复状态:从栈中恢复寄存器和返回地址。
  • 返回调用者:跳转回调用函数的下一条指令继续执行。

这些步骤带来了额外的开销,尤其是对于频繁调用的小函数。

2. 内联函数的实现

内联函数通过在编译阶段直接将函数体代码复制到调用点,消除了上述大部分开销:

  • 源代码分析:编译器在编译过程中会分析源代码,识别出标记为内联的函数调用。
  • 代码替换:编译器将内联函数的代码直接插入到调用该函数的位置,这个过程称为内联。
  • 局部变量处理:内联函数中使用的局部变量会变成在调用点处的临时变量。
  • 代码优化:编译器可能还会对内联后的代码进行进一步优化,比如常量折叠、死代码消除等,以提高效率。

3. 编译器优化策略

尽管程序员可以通过inline关键字建议编译器进行内联,但最终是否内联由编译器决定。编译器会考虑以下因素:

  • 函数大小:太大的函数内联可能导致代码膨胀。
  • 调用频率:频繁调用的小函数更适合内联。
  • 代码复杂度:复杂的函数内联可能不会带来明显的性能提升。
  • 性能与代码大小的权衡:编译器会评估内联带来的性能提升是否值得增加的代码大小。

4. 内联函数的限制

内联函数并非总是有利无弊。其主要限制包括:

  • 代码膨胀:内联函数会导致目标代码变大,可能影响缓存命中率和加载时间。
  • 编译时间增加:内联增加了编译器的工作量,可能导致编译时间变长。
  • 调试困难:内联后的代码难以跟踪和调试,因为源代码中的函数调用位置不再对应于机器码中的跳转指令。

结论

内联函数是编译器优化技术的一种,用于减少小而频繁调用的函数的运行时开销。它通过在编译时将函数体直接插入到调用点,避免了函数调用的开销,但可能增加代码大小和编译时间。

内联函数的优势

  1. 减少函数调用开销:通过避免函数调用的额外开销,内联函数可以提高程序的执行速度。
  2. 提高代码效率:内联函数在调用频繁且函数体较短的情况下特别有效,可以显著提升性能。

内联函数的局限性

  1. 代码膨胀:由于内联函数的代码会在每次调用处复制,这可能导致生成的二进制代码体积增大。
  2. 编译时间增加:内联函数增加了编译器的工作量,可能导致编译时间变长。
  3. 内存占用增加:更多的代码意味着可能需要更多的内存来加载和执行程序。

编译器优化

虽然可以通过inline关键字提示编译器进行内联,但最终是否内联取决于编译器的优化策略。编译器可能会基于函数大小、调用频率、代码复杂度等因素决定是否内联函数。有时,即使没有inline关键字,编译器也会选择内联函数以优化性能。

使用场景

内联函数最适合那些函数体较小、调用频繁的函数。对于复杂、计算密集型的函数,使用内联可能并不合适,因为带来的代码膨胀和编译时间增加可能超过性能提升的好处。

示例

#include <iostream>

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

int main() {
    int result = add(10, 20);
    std::cout << "Result: " << result << std::endl;
    return 0;
}

在这个例子中,add函数被声明为内联函数,如果它在程序中被频繁调用,使用内联可以提高程序的执行效率。然而,如果add函数包含复杂的逻辑或大量的代码,内联可能不是最佳选择。

面向对象

在C++中,this指针是一个非常重要且独特的概念,它在成员函数中扮演着核心角色。this指针是一个隐含的指针,指向调用成员函数的对象实例。理解this指针对于掌握面向对象编程至关重要。下面我们将深入探讨this指针的各个方面。

1. this指针的定义和作用

this指针是一个指向当前对象的指针,其类型为const限定的类类型指针。例如,如果类名为MyClass,则this指针的类型为MyClass* const this。这意味着this指针可以被用来访问对象的成员变量和成员函数,但它自身不能被修改。

2. this指针的使用场景

  • 访问成员变量this指针可以用于在成员函数内部访问对象的成员变量。
  • 区分局部变量和成员变量:当局部变量和成员变量同名时,this指针可以用于明确地区分两者。
  • 作为参数传递this指针可以作为参数传递给其他函数或成员函数,这在实现递归调用或链式调用等场景中尤为有用。
  • 返回当前对象:在某些情况下,成员函数可能需要返回调用它的对象,这时this指针可以派上用场。

3. 静态成员函数和this指针

静态成员函数没有this指针。这是因为在静态成员函数内部,没有特定的对象实例与之关联,所以this指针没有意义。静态成员函数只能访问静态成员变量和静态成员函数。

4. this指针的生命周期

this指针的生命周期与成员函数的调用周期相同。当成员函数开始执行时,this指针被创建并指向调用该函数的对象。当成员函数执行完毕,this指针也随之销毁。

5. this指针与const成员函数

在const成员函数中,this指针的类型变为const MyClass* const this。这意味着在const成员函数中不能修改对象的状态,即不能修改任何非static成员变量。

6. this指针与虚函数

当一个对象通过基类指针或引用来调用虚函数时,this指针仍然指向实际对象的地址,而不是基类对象的地址。这是多态性的基础,确保了调用正确版本的成员函数。

示例代码

class MyClass {
public:
    int data;

    void set(int value) {
        data = value;
    }

    void print() const {
        std::cout << "Data: " << this->data << std::endl;
    }
};

int main() {
    MyClass obj;
    obj.set(10);
    obj.print(); // 输出: Data: 10
    return 0;
}

在这个例子中,setprint都是成员函数,它们都可以通过this指针访问对象的data成员变量。print函数是const成员函数,因此它的this指针是const MyClass* const this类型。

总之,this指针是C++中面向对象编程的一个关键概念,它使得成员函数能够访问和操作所属对象的数据成员。理解this指针的特性和使用场景对于编写高效、正确的C++代码至关重要。

operator运算符重载

运算符重载(Operator Overloading)是C++中的一项强大特性,它允许程序员为自定义数据类型重新定义已存在的运算符的行为。通过运算符重载,用户定义的类型可以像内置类型(如int, float等)那样使用运算符,从而使得代码更加直观和自然。

运算符重载的基础

在C++中,运算符本质上是特殊的函数,它们具有预定义的符号,如+, -, *, /, =等。当这些运算符应用于用户定义的类型时,默认行为通常是不适用的,因为这些运算符原本是为内置类型设计的。运算符重载使得用户可以定义这些运算符在自定义类型上的行为,从而扩展了C++的表达能力。

运算符重载的规则

  1. 语法:运算符重载通过使用operator关键字和对应的运算符来实现,如operator+表示加法运算符的重载。

  2. 重载函数的声明:重载的运算符可以作为类的成员函数或友元函数。成员函数中的this指针隐含为左操作数,而友元函数需要显式地接收所有操作数。

  3. 参数:重载函数的参数数量取决于运算符的特性。例如,二元运算符如+通常需要两个参数,而一元运算符如-(负号)只需一个参数。

  4. 返回类型:重载函数的返回类型应该与运算符的预期行为相匹配。例如,重载+运算符通常返回一个与操作数类型相同的结果。

  5. 限制:有些运算符不能被重载,如::, ? :, ., .*, sizeof, alignof, typeid

  6. 不能改变运算符的优先级和结合性:重载运算符不会改变其原有的优先级和结合性。

示例:重载加法运算符

假设我们有一个复数类Complex,我们想要重载加法运算符+,以便两个Complex对象可以相加。

class Complex {
public:
    double real, imag;

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

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

int main() {
    Complex c1(3.0, 2.0);
    Complex c2(1.0, 7.0);
    Complex c3 = c1 + c2; // 调用重载的+
    return 0;
}

在这个例子中,Complex类的成员函数operator+接收另一个Complex对象作为参数,并返回一个新的Complex对象,其实部和虚部分别为两个操作数的实部和虚部之和。

运算符重载的注意事项

  1. 效率:重载运算符时应考虑效率,避免不必要的复制或分配。例如,可以使用引用或const引用作为参数。

  2. 语义一致性:重载的运算符应尽可能保持与原运算符的语义一致,以避免混淆。

  3. 互操作性:当与内置类型混合使用时,应确保重载运算符的行为与内置类型的行为协调一致。

  4. 运算符函数的调用:重载运算符通常通过操作符语法调用,而不是像普通函数那样使用圆括号。

运算符重载是C++中一项强大的特性,它增强了语言的表达能力和代码的可读性,但也需要谨慎使用,以避免引入复杂性和潜在的错误。

为什么要用operator以及operator有什么好处

运算符重载在C++中是一项非常有用的特性,它允许用户自定义数据类型的行为,使之与内置类型一样可以使用标准的运算符。下面是使用运算符重载的几个主要原因及其带来的好处:

1. 提升代码的可读性和自然性

  • 自然语法:运算符重载使得用户自定义的类型可以用类似内置类型的方式来使用运算符,如+, -, *, /等,这使得代码更直观,更接近数学或日常语言的表达方式。

  • 增强表达力:通过重载运算符,你可以为自定义类型提供类似于内置类型那样的操作,这使得代码更容易理解,因为读者可以使用熟悉的符号来理解复杂的数据结构之间的交互。

2. 方便的编程接口

  • 减少代码量:运算符重载可以减少代码量,因为你不必为每个操作都定义一个单独的函数。例如,a + badd(a, b)更简洁。

  • 提高代码重用性:重载运算符可以重用现有的代码和逻辑,使得同一组操作可以应用于多种不同的数据类型。

3. 更好的控制和灵活性

  • 定制行为:你可以根据需要为特定的数据类型定制运算符的行为,这提供了极大的灵活性,可以精确控制数据类型在不同场景下的操作方式。

  • 控制数据访问:通过重载运算符,你可以控制数据的访问和修改方式,确保数据的一致性和完整性。

4. 效率和性能

  • 避免不必要的拷贝:通过重载如赋值运算符=, 可以更高效地管理资源,避免不必要的对象拷贝,提高程序的性能。

  • 更直接的优化:编译器可以对内联的运算符重载函数进行优化,因为它们在调用点处被展开,这可能比普通函数调用更高效。

5. 面向对象编程的增强

  • 支持多态性:运算符重载可以与多态性相结合,使得基类的运算符可以被派生类重写,从而在运行时表现出不同的行为。

  • 封装和继承:运算符重载可以更好地支持封装和继承原则,允许在继承层次结构中保持运算符行为的一致性。

示例

假设你有一个Vector类,你可以重载+运算符,使得两个Vector对象可以通过简单的a + b语法相加,而不需要调用额外的add函数。这不仅让代码更简洁,而且更符合直觉。

总的来说,运算符重载提供了更高级别的抽象,使得C++程序的编写更加高效、直观和优雅。然而,这也要求开发者在使用时要小心,确保重载的运算符行为合理且不会引起混淆。

C++ template模板

在这里插入图片描述

C++模板是语言中一个非常强大的特性,它允许程序员编写通用的代码,能够处理多种数据类型,而无需为每种类型重复编写相同的逻辑。模板分为函数模板和类模板两种,它们的核心思想是在编译时根据具体类型生成特定的代码实例。

1. 函数模板

函数模板允许你定义一个函数,它可以接受任意类型的参数,并返回相应类型的结果。模板函数的基本语法如下:

template<typename T>
T myFunction(T arg) {
    // 函数体
}

这里,typename关键字用于指定模板参数的类型,T是一个模板参数,代表任意类型。当你调用模板函数时,编译器会根据传入的参数类型生成相应的函数实例。

示例:函数模板
template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

int main() {
    int x = 10, y = 20;
    double a = 3.14, b = 2.71;

    std::cout << max(x, y) << std::endl;  // 输出: 20
    std::cout << max(a, b) << std::endl;  // 输出: 3.14
    return 0;
}

2. 类模板

类模板允许你定义一个类,其中某些成员类型可以是模板参数。这样,你可以基于不同的类型实例化不同的类。

示例:类模板
template<typename T>
class MyVector {
private:
    T* data;
    size_t capacity, size;
public:
    MyVector(size_t initialCapacity) : capacity(initialCapacity), size(0), data(new T[capacity]) {}

    ~MyVector() {
        delete[] data;
    }

    void push_back(const T& value) {
        if (size == capacity) {
            capacity *= 2;
            T* newData = new T[capacity];
            for (size_t i = 0; i < size; ++i) {
                newData[i] = data[i];
            }
            delete[] data;
            data = newData;
        }
        data[size++] = value;
    }

    T& operator[](size_t index) {
        return data[index];
    }
};

int main() {
    MyVector<int> intVec(10);
    MyVector<double> doubleVec(5);

    for (int i = 0; i < 10; ++i) {
        intVec.push_back(i);
    }

    for (double d = 0.0; d < 5.0; d += 1.0) {
        doubleVec.push_back(d);
    }

    std::cout << intVec[5] << std::endl;  // 输出: 5
    std::cout << doubleVec[3] << std::endl;  // 输出: 3.0
    return 0;
}

3. 模板参数

模板参数可以是类型参数(使用typenameclass关键字)、非类型参数(整型、枚举、指针等)和模板模板参数(其他模板)。例如:

template<typename T, typename U = int>
class MyClass {
    // ...
};

这里,U是一个有默认值的模板参数。

4. 模板特化

模板特化允许你为特定类型提供不同的实现。例如,你可以为你的模板类提供一个int类型的特化版本。

template<>
class MyVector<int> {
    // 专为int类型提供的实现
};

5. 模板元编程

模板元编程是一种在编译时执行计算的技术,利用模板和模板参数来生成代码。C++11及更高版本中引入的constexpr和可折叠模板参数使元编程更加灵活和强大。

总结

C++模板是实现代码重用和泛型编程的强大工具。通过模板,你可以编写一次代码,就能处理多种数据类型,这极大地提高了代码的可维护性和效率。然而,模板的使用也应当谨慎,过度使用或不当使用可能会导致代码难以理解和调试。

内存操作

在C++中,operator newoperator delete是一对特殊的运算符,它们分别用于内存的分配和释放。这两个运算符使得C++能够实现更高级别的内存管理功能,比如异常安全的动态内存分配和自定义的内存管理策略。

operator new

operator new用于从自由存储区分配未初始化的内存块。基本形式如下:

void* operator new(std::size_t size);

这里的size参数指定了需要分配的字节数。operator new返回一个指向分配的内存的指针,如果分配失败,则可能抛出std::bad_alloc异常。

此外,C++还提供了几个重载版本的operator new,包括带有对齐要求的版本和带有额外参数的版本,如placement new

void* operator new(std::size_t size, const std::nothrow_t&) noexcept;
void* operator new(std::size_t size, std::align_val_t alignment);
void* operator new(std::size_t size, void* ptr) noexcept;
  • nothrow版本尝试分配内存但不抛出异常,而是返回nullptr
  • alignment版本允许你指定内存对齐方式。
  • placement new不分配内存,而是直接在给定地址上构造对象。

operator delete

operator delete用于释放由operator new分配的内存。基本形式如下:

void operator delete(void* ptr) noexcept;

这里的ptr是指向要释放的内存的指针。operator delete不会抛出异常,但如果内存没有被正确地释放,程序的行为是未定义的。

同样,operator delete也有几个重载版本:

void operator delete(void* ptr, const std::nothrow_t&) noexcept;
void operator delete(void* ptr, std::size_t size) noexcept;
void operator delete(void* ptr, std::align_val_t alignment) noexcept;
  • nothrow版本与operator newnothrow版本对应,表明不会抛出异常。
  • sizealignment版本允许传递额外的信息,这在自定义的内存管理器中可能有用。

自定义newdelete

你可以在类中重载operator newoperator delete,从而实现特定于类的内存管理。这通常用于实现池分配器、缓存机制或确保资源的正确释放。例如:

class MyClass {
public:
    static void* operator new(std::size_t size) {
        // 自定义的内存分配逻辑
        return malloc(size);
    }

    static void operator delete(void* ptr) noexcept {
        // 自定义的内存释放逻辑
        free(ptr);
    }
};

异常安全的内存管理

使用newdelete时,应当注意异常安全。例如,在复杂的构造函数中,如果构造过程中发生异常,应该确保已经分配的内存被正确释放,这通常通过使用RAII(Resource Acquisition Is Initialization)原则来实现。

operator newoperator delete是C++中用于动态内存管理的关键部分。它们允许你在运行时分配和释放内存,同时也提供了自定义内存管理的机会。然而,错误的使用会导致内存泄漏或其他未定义行为,因此在使用时需要特别小心。正确使用这些运算符对于编写高效、健壮的C++程序至关重要。

/*

operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间

失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否

则抛异常。

*/

内存泄露的危害

什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内

存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对

该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现

内存泄漏会导致响应越来越慢,最终卡死。

void MemoryLeaks()
{
   // 1.内存申请了忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  
  // 2.异常安全问题
  int* p3 = new int[10];
  
  Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  
  delete[] p3;
}

new delete 原理解析

在C++中,newdelete是用于动态内存管理的核心操作符,它们允许程序员在运行时分配和释放内存。下面将深入解析newdelete的核心原理,包括它们如何工作,以及它们与C语言中mallocfree函数的区别。

new操作符

new操作符在C++中有两种基本形式:

  1. 单个对象的分配

    T* ptr = new T;
    

    这里T是需要分配内存的类型,new操作符会分配足够的内存来存储一个T类型的对象,并返回一个指向该内存的指针。

  2. 数组的分配

    T* arr = new T[n];
    

    这里n是要分配的元素数量,new操作符会分配足够存储nT类型对象的内存,并返回指向第一个元素的指针。

new操作符的核心原理
  • 内存分配new操作符首先调用底层的内存分配函数(通常是malloc或其等效函数)来请求内存。
  • 构造函数调用:一旦内存分配成功,new操作符会调用适当的构造函数来初始化对象。如果是数组,每个元素都会被初始化。
  • 异常处理:如果内存分配失败,new操作符会抛出std::bad_alloc异常。

delete操作符

new相对应,delete操作符也有两种形式:

  1. 删除单个对象

    delete ptr;
    

    这会调用ptr指向的对象的析构函数,然后释放内存。

  2. 删除数组

    delete[] arr;
    

    这会依次调用数组中每个对象的析构函数,然后释放整个数组的内存。

delete操作符的核心原理
  • 析构函数调用delete操作符首先调用对象的析构函数。析构函数负责清理对象在构造时分配的任何资源。
  • 内存释放:析构函数调用完成后,delete操作符会调用底层的内存释放函数(通常是free或其等效函数)来释放内存。

newdeletemallocfree的区别

  1. 构造和析构newdelete会自动调用构造函数和析构函数,而mallocfree仅处理内存分配和释放,不会调用构造或析构函数。
  2. 异常处理new在内存分配失败时会抛出异常,而malloc在失败时返回nullptr
  3. 类型安全性newdelete是类型安全的,它们知道所分配和释放的类型;而mallocfree是类型不可知的,需要程序员手动转换和管理类型。

newdelete是C++中动态内存管理的关键,它们不仅提供了内存的分配和释放,还自动处理了构造和析构,使得资源管理更加安全和方便。然而,错误的使用newdelete(如忘记释放内存或错误地释放内存)会导致内存泄漏和程序崩溃。因此,正确和谨慎地使用newdelete是非常重要的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1897427.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024-07-05 base SAS programming学习笔记9(variables)

1.在数据集增加累加变量值&#xff08;SUM&#xff09; 求和语句(SUM STATEMENT)&#xff1a;variableexpression variable是累积求和的变量名&#xff0c;为数值型&#xff0c;默认初始值为0&#xff1b;该variable值则会保留到一个观测 当expression有缺失值&#xff0c;在求…

深度学习Week19——学习残差网络和ResNet50V2算法

文章目录 深度学习Week18——学习残差网络和ResNet50V2算法 一、前言 二、我的环境 三、论文解读 3.1 预激活设计 3.2 残差单元结构 四、模型复现 4.1 Residual Block 4.2 堆叠Residual Block 4.3. ResNet50V2架构复现 一、前言 &#x1f368; 本文为&#x1f517;365天深度学…

v-html 空格/换行不生效

接口返回的内容如下&#xff1a;有空格有换行&#xff0c;但 使用v-html无效 需加css样式 white-space: pre-wrap; <div class"pretty-html" v-html"Value"></div>.pretty-html {white-space: pre-wrap; /* 保留空格和换行&#xff0c;并允许…

OZON怎么查看竞品数据,OZON怎么找竞品数据

在跨境电商的激烈竞争中&#xff0c;了解和分析竞品数据是每一位卖家优化销售策略、提升市场竞争力的关键步骤。OZON作为俄罗斯领先的电商平台&#xff0c;为卖家提供了丰富的数据分析工具&#xff0c;而萌啦ozon数据作为第三方数据分析平台&#xff0c;更是为卖家提供了更为全…

Linux 进程与计划任务管理

一、程序、进程、线程的概念 1. 程序&#xff1a;是指一组指示计算机或其他具有信息处理能力装置执行动作或做出判断的指令&#xff0c;通常用某种程序设计语言编写&#xff0c;运行于某种目标计算机体系结构上 2. 进程&#xff1a;是计算机中的软件程序关于某数据集合上的一次…

某yi逆向sign值

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;不提供完整代码&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 本文章未经…

pdf可以删除其中一页吗?6个软件教你快速进行pdf编辑

pdf可以删除其中一页吗&#xff1f;6个软件教你快速进行pdf编辑 编辑PDF文件并删除特定页面是处理文档时常见的需求&#xff0c;特别是在需要定制或精简文件内容时。以下是几款广受欢迎的PDF编辑软件&#xff0c;它们提供了强大的页面删除功能&#xff0c;帮助用户轻松管理和修…

重新定义 AI 部署效率与性能!ZOLOZ 如何借助 AC2 实现低成本高性能的提优?| AI 案例推荐

AI 容器镜像——面向云上全容器场景 王坚院士提到&#xff0c;算力是人工智能发展的三要素之一&#xff0c;而云计算是最好的提供算力的技术手段&#xff0c;英伟达的黄仁勋也表达过类似的观点。可以说&#xff0c;大模型时代下的 AI&#xff0c;天然就是在云场景里的。 容器…

SpringBoot-第一天学习

SpringBoot介绍-约定大于配置 SpringBoot是在Spring4.0基础上开发的&#xff0c;不是替代Spring的解决方案&#xff0c;而是和Spring框架结合并进一步简化Spring搭建和开发过程的。 如何简化&#xff1f;就是通过提供默认配置等方式让我们更容易&#xff0c;集成了大量常用的…

景区智能厕所系统,打造智能化,人性化公共空间

在智慧旅游的大潮中&#xff0c;景区智能厕所系统正逐渐成为提升公共空间智能化、人性化水平的关键载体。作为智慧城市建设的重要组成部分&#xff0c;智能厕所系统不仅解决了传统公厕存在的诸多问题&#xff0c;更通过科技的力量&#xff0c;为游客创造了更加舒适、便捷的如厕…

StreamSets: 数据采集工具详解

欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;欢迎订阅相关专栏&#xff1a; 欢迎关注微信公众号&#xff1a;野老杂谈 ⭐️ 全网最全IT互联网公司面试宝典&#xff1a;收集整理全网各大IT互联网公司技术、项目、HR面试真题. ⭐️ AIGC时代的创新与未来&a…

c/c++ 程序运行的过程分析

c/c编译基础知识 GNU GNU&#xff08;GNU’s Not Unix!&#xff09;是一个由理查德斯托曼&#xff08;Richard Stallman&#xff09;在1983年发起的自由软件项目&#xff0c;旨在创建一个完全自由的操作系统&#xff0c;包括操作系统的内核、编译器、工具、库、文本编辑器、邮…

渲染农场渲染真的很贵吗?如何正确使用云渲染农场?

作为渲染行业主流的技术服务“渲染农场"&#xff0c;一直都是备受大家关注&#xff0c;渲染农场最核心的在于充足的计算机算力&#xff0c;结合3D软件支持多台机器渲染的特点&#xff0c;租用渲染农场的机器帮助你快速的解决你的渲染项目。 虽然说渲染农场需要支付一定的…

Astro新前端框架首次体验

Astro新前端框架首次体验 1、什么是Astro Astro是一个静态网站生成器的前端框架&#xff0c;它提供了一种新的开发方式和更好的性能体验&#xff0c;帮助开发者更快速地构建现代化的网站和应用程序。 简单来说就是&#xff1a;Astro这个是一个网站生成器&#xff0c;可以直接…

MinIO:开源对象存储解决方案的领先者

MinIO:开源对象存储解决方案的领先者 MinIO 是一款开源的对象存储系统&#xff0c;致力于提供高性能、可伸缩、安全的数据存储解决方案。 官方解释&#xff1a;MinIO 是一个基于Apache License v2。0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口&#xff0c;非常适…

因版本冲突导致logback的debug日志不打印

因框架调整&#xff0c;降级了logback的版本号&#xff0c;由1.3.12降级为1.2.11&#xff08;因框架限制&#xff0c;只能采用1.2版本&#xff09;&#xff0c;降级后发现debug日志无法打印出来&#xff0c;logback.xml配置文件不生效。后排查发现是与slf4j的版本兼容问题 依赖…

以某头部基金实践为例,验证深信服超融合对TA系统承载能力

TA&#xff08;Transfer Agent&#xff09;开放式基金登记过户系统是交易管理系统的重要组成部分&#xff0c;是登记注册机构向投资者提供账户管理、份额登记、交易清算、红利发放、持有人名册保管的综合服务系统。 作为开放式基金运作的核心系统之一&#xff0c;承担着投资者…

Qt 加载图片的几种方式 以及加载 loading

项目中经常使用加载图片&#xff1a; 常用有两种方式&#xff1a; 1.使用 QWidget 加载图片&#xff1a; 效果&#xff1a; 样例源码&#xff1a; int pict_H ui->widgetImage->height();int pict_W ui->widgetImage->width();ui->widgetImage->setFixe…

【车载开发系列】GIT安装详细教程

【车载开发系列】GIT安装详细教程 【车载开发系列】GIT安装详细教程 【车载开发系列】GIT安装详细教程一. GIT软件概念二. GIT安装步骤三. GIT安装确认三. GIT功能使用1&#xff09;Git Bash2&#xff09;Git CMD3&#xff09;Git FAQs4&#xff09;Git GUI 一. GIT软件概念 G…

comsol随机材料参数赋值

comsol随机材料参数赋值 在comsol中定义外部matlab函数 在comsol中定义外部matlab函数 首选项&#xff0c;安全性&#xff0c;允许 材料中&#xff0c;将杨氏模量更改为变量函数 计算 应力有波动&#xff0c;可见赋值成功 也可以看到赋值的材料参数&#xff1a;