重庆网站建设最大,广东顺德网站建设,丰台网站建设是什么,自己做的网站怎么发布win71. 内存有哪几种类型#xff1f;
内存分为五个区#xff0c;堆#xff08;malloc#xff09;、栈#xff08;如局部变量、函数参数#xff09;、程序代码区#xff08;存放二进制代码#xff09;、全局/静态存储区#xff08;全局变量、static变量#…1. 内存有哪几种类型
内存分为五个区堆malloc、栈如局部变量、函数参数、程序代码区存放二进制代码、全局/静态存储区全局变量、static变量和常量存储区常量。此外C中有自由存储区new一说。 全局变量、static变量会初始化为缺省值0而堆和栈上的变量是随机的不确定的。
2. 堆和栈的区别
堆存放动态分配的对象——即那些程序运行时动态分配的对象比如new出来的对象其生存周期由程序控制。栈用来保存定义在函数内的非static对象如局部变量仅在其定义的程序块运行时才存在。静态内存用来保存static对象类static数据成员以及定义在任何函数外部的变量static对象在使用之前分配程序结束时销毁。栈和静态内存的对象由编译器自动创建和销毁。 解释 堆Heap堆是程序员通过代码中的分配函数比如malloc函数手动分配和释放的内存区域。如果你的存储需求在编译时是未知的或者你需要在程序的不同部分和/或在不同的时间点分配和释放内存你应该使用堆。堆内存的主要优点是灵活性但是使用堆内存需要更多的CPU时间因为必须在运行时搜索和管理堆。堆内存分配失败时通常会返回一个空指针。
栈Stack
栈是自动分配和释放的内存区域通常用于存储局部变量和函数调用的信息。
这两者的差异可以总结如下:
生命周期堆中的对象由程序员创建和销毁。只要程序员不销毁它它就会一直存在。而栈中的对象在函数结束时自动销毁。存储空间在很多系统中栈的空间远小于堆空间。因此栈可用于存储小型数据例如函数调用和局部变量而堆则能够分配大量的内存空间。管理方式程序员负责管理堆中的内存包括分配和释放。而栈内存由编译器自动管理这使得在栈上分配内存的速度比在堆上快。
3. 堆和自由存储区的区别
总的来说堆是C语言和操作系统的术语是操作系统维护的一块动态分配内存自由存储是c中通过new与delete动态分配和释放对象的抽象概念。他们并不是完全一样。
从技术上来说堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存它提供了动态分配的功能。当运行程序调用malloc()时就会从中分配稍后调用free可把内存交还。而自由存储是C中通过new和delete动态分配和释放对象的抽象概念通过new来申请的内存区域可称为自由存储区。基本上所有的C编译器默认使用堆来实现自由存储也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来实现这时由new运算符分配的对象说它在堆上也对说他在自由存储区上也正确。
4. 程序编译的过程
程序编译的过程中就是将用户的文本形式的源代码c转化成计算机可以直接执行的机器代码的过程。主要经过四个阶段预处理编译汇编链接。具体实例如下
#include stdio.h
int main()
{printf(happy new year!\n);return 0;
}其编译过程如下
5. 计算机内部如何存储负数和浮点数
负数比较容易就是通过一个标志位和补码来进行表示。取反加一
对于浮点型的数据采用单精度类型float和双精度类型double来存储float数据占用32bitdouble数据占用64bit。我们在声明一个变量float f 2.5的时候无论是单精度还是双精度在存储中都分为三个部分
符号位Sign0代表正1代表负指数位Exponent用于存储科学计数法中的指数数据并且采用移位存储移码和补码只相差一个符号位尾数部分Mantissa尾数部分
其中float的存储方式如下所示
double的存储方式 6. 左值和右值
不是很严谨的来说左值指的是既能够出现在等号左边也能出现在等号右边的变量*或表达式右值指的则是只能出现在等号右边的变量或表达式。举例来说我们定义的变量a就是一个左值而malloc返回的就是一个右值。或者左值就是在程序中能够寻值的东西右值就是一个具体的值或对象没法取到他的地址的东西因此无法对右值赋值但是右值并不是不可修改的比如自定义的class可以通过它的成员函数来修改右值。 归纳一下就是 可以取地址的有名字的非临时的就是左值不能取地址的没有名字的临时的通常生命周期就在某个表达式之内的就是右值。
7. 什么是内存泄漏面对内存泄漏和指针越界你有哪些方法通常采用哪些方法来避免和减少这类错误
用动态存储分配函数动态开辟的空间在使用完毕后没及时释放结果导致一直占用该内存单元即为内存泄漏。
面对内存泄漏和指针越界的情况有几种常见的方法来检测和预防这些问题 根据设计使用智能指针如shared_ptr、unique_ptr、容器如vector、string等、资源获取即初始化RAII等C特性来管理内存。这些方法能够自动释放不再需要的内存这样就可以减少内存泄漏的可能性。 使用工具有许多工具可以帮助检测内存泄漏和指针越界比如Valgrind、AddressSanitizer等。这些工具可以在运行时检测程序中的内存问题并提供详细的报告来帮助你找到问题的原因。 代码审查和测试通过团队成员之间的代码审查以及编写针对代码的单元测试和集成测试我们可以在代码被合并到主分支之前就发现并修复许多问题。
预防方法包括
尽可能使用RAII原则资源获取即初始化以确保资源在不再需要时被正确释放。如果可能使用标准库的容器和算法而不是手动管理内存和数组。使用智能指针管理需要在堆中分配的资源。在合适的情况下使用异常安全的技术。养成良好的编程习惯如在使用完动态分配的内存后立即释放尽量避免裸指针的使用等。
8. C/C引用和指针的区别 指针是一个实体需要分配内存空间。引用只是变量的别名不需要分配内存空间。 引用在定义的时候必须进行初始化并且不能够改变。指针在定义的时候不一定要初始化并且指向的空间可变。不能有引用的值不能为NULL 有多级指针但是没有多级引用只能有一级引用。 指针和引用的自增运算结果不一样。指针是指向下一个空间引用是应用的变量值加一 sizeof引用得到的是所指向的变量的大小而sizeof指针得到的是指针本身的大小。 引用访问一个变量是直接访问而指针访问一个变量是间接访问。 使用指针前最好做类型检查防止野指针的出现。 引用底层是通过指针实现的。 作为参数时也不同传指针的实质是传值传递的是指针的地址传引用的实质是传地址传递的是变量的地址。
9. 从汇编层去解释一下引用 x的地址是ebp-4b的地址是ebp-8因为栈内的变量内存是从高往低进行分配的所以b的地址比x的低。
lea eax【ebp-4】这条语句将x的地址ebp-4放入寄存器eax mov dword ptr 【ebp-8】eax 这条语句将eax的值放入b的地址ebp-8
上面两条语句的作用将x的地址传入变量b中这不和将某个变量的地址存入指针变量是一样的么 所以从汇编的角度看的却引用是通过指针拉实现的。
https://blog.csdn.net/songbijian/article/details/132507421
10. 讲一讲封装、继承和多态是什么
封装将具体实现过程和数据封装成一个函数只能通过接口进行访问降低耦合性使类成为一个具有内部数据的自我隐藏能力、功能独立的软件模块。 意义保护或防止代码在无意之中被破坏保护类中的成员不让类中以外的程序直接访问或修改只能通过提供的公共接口访问。
继承子类继承父类的特征和行为复用了基类的全体数据和成员函数具有从基类复制而来的数据成员和成员函数基类私有成员可以被继承但是无法被访问其中构造函数、析构函数、友元函数、静态数据成员、静态成员函数都不能被继承。基类中成员的访问方式只能决定派生类能否访问他们。增强了代码耦合性当父类中的成员变量或者类本身被final关键字修饰时修饰的类不能被继承修饰的成员变量不能被重写或修改。 继承的类型有三种公有继承、保护继承、私有继承。无论哪种继承方式基类的私有成员都会被继承。但对于派生类而言无论是公有继承、保护继承还是私有继承基类的私有成员在派生类中都无法直接访问 意义基类的程序代码可以被派生类复用提高了软件复用的效率缩短了软件开发的周期。
多态不同继承类的对象对同一消息做出不同的响应基类的指针指向或绑定到派生类的对象使得基类指针呈现不同的表现形式。 意义对已存在的代码具有可替代性对代码具有可扩充性新增子类不会影响已存在类的各种性质在程序中体现了灵活多样的操作提高了使用效率简化了对应用代码的编写和修改过程。
解释
子类不能直接访问基类的私有private成员那些私有成员仍会成为子类对象的一部分。 这句话的意思是虽然从派生类子类中无法直接访问基类父类的私有成员但这些私有成员依然会占用派生类对象的内存这就是说它们在物理上是成为派生类对象的一部分。
举个例子来说我们有一个基类Base它有一个私有成员baseVal还有一个公有成员函数setBaseVal可以设置baseVal的值和一个公有成员函数getBaseVal可以获取baseVal的值。
class Base {
private:int baseVal;
public:void setBaseVal(int val) {baseVal val;}int getBaseVal() {return baseVal;}
}; 然后我们有一个从Base派生出来的类Derived它有一个公有成员derivedVal。
class Derived : public Base {
public:int derivedVal;
}; 这时候我们创建一个Derived对象
Derived d; 这个Derived对象会包含一个Base对象作为它的一部分该Base对象包含baseVal成员。虽然从Derived类的方法中无法直接访问这个baseVal成员但这个baseVal成员实际上是存在于d对象的内存中的。你可以通过Base类的公有方法setBaseVal和getBaseVal来间接地修改和获取baseVal的值。
公有继承、保护继承、私有继承 当一个类从另一个类派生时派生类通常会接收到基类的成员变量和方法。但基类的哪些成员能被派生类接收到以及派生类外部代码如何访问这些接收到的成员则取决于派生类的继承类型即公有继承、保护继承和私有继承。公有public继承在此继承方式下父类的公有和保护成员都会成为派生类的成员且它们在派生类中的访问属性保持不变公有成员仍为公有保护成员仍为保护。父类的私有成员无法从派生类直接访问但会被派生类物理上继承即占用内存空间。 class Base {public:int public_member;protected:int protected_member;private:int private_member;};class Derived : public Base {// 可以访问public_member和protected_member// 不能访问private_member};保护protected继承在此继承方式下父类的公有和保护成员都会成为派生类的成员且它们在派生类中的访问属性都将变为保护。父类的私有成员无法从派生类直接访问但会被派生类物理上继承。 class Derived : protected Base {// 可以访问public_member和protected_member// 不能访问private_member};Derived d;// d无法访问public_member和protected_member私有private继承在此继承方式下父类的公有和保护成员都会成为派生类的成员且它们在派生类中的访问属性都将变为私有。父类的私有成员无法从派生类直接访问但会被派生类物理上继承。 class Derived : private Base {// 可以访问public_member和protected_member// 不能访问private_member};Derived d;// d无法访问public_member和protected_member 这些规则主要应用于派生类内部即派生类的方法可以访问到的成员。对于派生类对象它可以访问的成员由派生类的访问属性和继承类型共同决定。
11. 多态的实现原理实现方式是什么以及多态的优点
实现方式多态分为动态多态动态多态是利用虚函数实现运行时的多态即在系统编译的时候并不知道程序要调用哪个函数只有在运行到这里的时候才能确定接下来会跳转到哪一个函数和静态多态又称编译期多态即在系统编译期间就可以确定程序将要执行那个函数。 其中动态多态是通过虚函数实现的虚函数是类的成员函数存在储存虚函数指针的表叫虚函数表虚函数表是一个存储类成员虚函数的指针每个指针都指向调用它的地方当子类调用虚函数时就会去虚表里找到自己对应的函数指针从而实现谁调用实现谁“从而实现多态。 而静态多态则是通过函数重载函数名相同参数不同两个函数在同一个作用域运算符重载和重定义又叫隐藏指的是在继承关系中子类实现了一个和父类一样名字的函数(只关注函数名和参数与返回值无关)这样的话子类的函数就把父类的同名函数给隐藏了。隐藏只与函数名有关与参数没有关系来实现的。
优点加强代码的可扩展性可替换性增强程序的灵活性提高使用效率简化对应用代码的编写和修改过程。 #include iostream// 函数重载示例
void print(int num) {std::cout Printing integer: num std::endl;
}void print(double num) {std::cout Printing double: num std::endl;
}int main() {print(5); // 调用第一个 print 函数参数类型为 intprint(3.14); // 调用第二个 print 函数参数类型为 doublereturn 0;
}//当使用函数重载时编译器会根据函数的签名参数类型和个数来确定调用哪个函数。这就是静态多态的一个例子。
//在这个示例中有两个名为 print 的函数一个接受整数参数另一个接受双精度浮点数参数。在调用 print 函数时编译器根据传递的参数类型来确定应该调用哪个函数。这是静态多态的一种表现形式。 #include iostream// 基类
class Shape {
public:// 虚函数virtual void draw() {std::cout Drawing shape std::endl;}
};// 派生类
class Circle : public Shape {
public:// 重写基类的虚函数void draw() override {std::cout Drawing circle std::endl;}
};// 派生类
class Square : public Shape {
public:// 重写基类的虚函数void draw() override {std::cout Drawing square std::endl;}
};int main() {Shape* shapePtr;Circle circle;Square square;shapePtr circle;shapePtr-draw(); // 调用 Circle 类的 draw 函数shapePtr square;shapePtr-draw(); // 调用 Square 类的 draw 函数return 0;
}//在这个示例中Shape 类有一个虚函数 draw派生类 Circle 和 Square 分别重写了这个虚函数。在 main 函数中通过基类指针调用 draw 函数根据指针所指对象的实际类型决定调用的是派生类的哪个 draw 函数。这是动态多态的一种表现形式。
12. final的标识符的作用是什么
放在类的后面表示该类无法继承也就是阻止了从类的继承放在虚函数后面该虚函数无法被重写表示阻止虚函数的重载。 class Base {
public:virtual void foo() final; // foo函数不能被派生类覆盖
};class Derived : public Base {
public:void foo(); // 错误试图覆盖Base类的final函数
};class FinalClass final {// 这个类不能被继承
};// 错误试图从FinalClass类继承一个子类
class DerivedFromFinal : public FinalClass {
};
13. 虚函数是怎么实现的他存放在哪里内存的哪个区什么时候生成的
^9383bd
在C中虚函数的实现基于两个关键概念虚函数表和虚函数指针。
虚函数表每个包含虚函数的类都会生成一个虚函数表其中存储着该类中所有虚函数指针或虚表指针。这个指针指向该对象对应的虚函数从而让程序能够动态的调用虚函数。
虚函数指针在对象的内存布局中编译器会添加一个额外的指针成为虚函数指针或虚表指针。这个指针指向该对象对应的虚函数表从而让程序能够动态的调用虚函数。
当一个基类指针或引用调用虚函数时编译器会使用虚表指针来查找该对象对应的虚函数表并根据函数在虚函数表中的位置来调用正确的虚函数。
在编译阶段生成虚函数和普通函数一样存放在代码段只是他的指针又存放在虚表之中。
解释 C 中的虚函数是通过一个称为 vtable 的虚函数表来实现的。虚函数表是一个存储类成员函数指针的数组当一个类被声明为虚函数时编译器会为这个类生成一个虚函数表。
虚函数的实现可以大致分为下面几个步骤 在编译阶段编译器检查每一个带有虚函数的类。对于每一个这样的类编译器都会生成一个虚函数表。这个虚函数表是一个存储函数指针的数组函数指针指向类的虚函数。 然后对于每一个对象编译器会在对象的内存布局的开头部分添加一个指针这个指针指向该对象所属类的虚函数表。这个指针是在对象被创建时自动添加的。 当我们通过基类指针调用一个虚函数时编译器首先会访问这个基类指针指向的对象的虚函数表指针然后通过这个虚函数表指针找到虚函数表最后在虚函数表中查找并调用虚函数。
虚函数所在的内存区域取决于它们是如何存储的。通常函数包括虚函数的代码存储在代码区而虚函数表存储函数指针的数组和虚函数表指针在每个对象中则存储在数据区的动态分配部分。
虚函数表是在编译阶段就已经生成的然而虚函数表指针是在运行时具体的对象被创建时才添加到对象的内存布局中的。对于虚函数调用的解析则是在运行时进行的这就是所谓的动态绑定或运行时多态。
14. 智能指针的本质是什么他们的实现原理是什么
智能指针的本质是一个封装了一个原始C指针的类模板为了确保动态内存的安全性而产生。实现原理是通过一个对象存储需要被自动释放的资源然后依靠对象的析构函数来释放资源。 #include iostream
#include memory // 包含智能指针头文件int main() {// 使用 std::unique_ptr 来管理动态分配的内存std::unique_ptrint ptr(new int(42));// 访问动态分配的内存std::cout Value stored in dynamic memory: *ptr std::endl;// 当 unique_ptr 超出作用域时它会自动释放所管理的内存// 不需要手动调用 deletereturn 0;
}//在这个示例中我们使用 std::unique_ptr 来管理动态分配的整数内存。在创建 std::unique_ptr 对象 ptr 时我们将 new int(42) 返回的指针传递给了它。std::unique_ptr 对象负责管理这块内存当 ptr 超出作用域时它会自动调用 delete 来释放所管理的内存从而避免了内存泄漏的风险。//这就是智能指针的作用通过对象管理资源利用析构函数自动释放资源提高了程序的安全性和可靠性。 //析构函数是在对象被销毁时自动调用的特殊成员函数。它的名称由波浪号~加上类名构成没有返回类型也不接受任何参数。析构函数通常用于在对象被销毁之前执行一些清理工作比如释放动态分配的内存、关闭文件、释放资源等。//当对象超出其作用域、被删除或者程序结束时其析构函数会被调用。对于动态分配的对象使用 new 关键字创建的对象当调用 delete 来释放这些对象时其析构函数也会被调用。 %%
#include iostreamclass MyClass {
public:// 构造函数MyClass() {std::cout Constructor called std::endl;}// 析构函数~MyClass() {std::cout Destructor called std::endl;}
};int main() {{MyClass obj; // 创建 MyClass 对象} // 对象超出作用域析构函数被调用MyClass* ptr new MyClass(); // 创建动态分配的 MyClass 对象delete ptr; // 释放动态分配的对象析构函数被调用return 0;
}
15. 匿名函数的本质是什么他的优点是什么
匿名函数本质上是一个对象在其定义的过程中会创建出一个栈对象内部通过重载符号实现函数调用的外表。
优点使用匿名函数可以免去函数的声明和定义。这样匿名函数仅在调用函数的时候才会创建函数对象而调用结束后立即释放所以匿名函数比非匿名函数更节省空间。 #include iostreamint main() {// 定义一个匿名函数并将其存储在变量 add 中auto add [](int a, int b) {return a b;};// 使用匿名函数进行加法运算int result add(3, 4);std::cout Result of addition: result std::endl;return 0;
}#include iostream
#include vector
#include algorithmint main() {// 定义一个存储匿名函数的容器std::vectorstd::functionint(int, int) operations;// 添加匿名函数到容器中operations.push_back([](int a, int b) { return a b; });operations.push_back([](int a, int b) { return a - b; });operations.push_back([](int a, int b) { return a * b; });// 使用容器中的匿名函数进行计算int x 10, y 5;for (const auto op : operations) {std::cout Result: op(x, y) std::endl;}return 0;
}
解释
匿名函数也被称为lambda函数原则上就是一个没有名字的函数。在许多编程语言中我们可以创建这样一个函数并且可以在需要使用函数的地方即时定义一个匿名函数。这种函数的好处是**可以在不需要创建完整函数定义即避免了定义函数的开销的情况下实现简单的、临时的功能。
在函数的本质层面上匿名函数并无两样。和命名函数一样它们也可以接收参数并且可以有返回值。匿名函数或者说lambda函数通常有两个关键特点
匿名顾名思义匿名函数是没有显式名称的不像正常函数那样通过def function_name的方式命名。内联定义匿名函数通常在代码中需要使用函数的地方定义而不是在代码的其他地方。这意味着可以在很小的范围例如一个函数中或者一个特定的代码块中内定义和使用它们。
匿名函数最常见的使用场景是在需要小的函数作为其他函数参数如map函数排序函数等的时候。事实上一个非常常见的例子就是在Python的map函数或者列表解析式中使用匿名函数。例如
numbers [1, 2, 3, 4]
squares map(lambda x: x ** 2, numbers) 在这个例子中lambda x: x ** 2 就是一个匿名函数它表示一个取一个参数x并返回x的平方的函数。我们在这里定义了这个函数并且立即将其用作了map函数的第一个参数而无需提前定义。这就是匿名函数的一种典型用法。
16. 右值引用是什么为什么要引入右值引用
右值引用为一个临时变量取别名他只能绑定到一个临时变量或表达式将亡值上。实际开发中我们可能需要对右值进行修改实现移动语义时就需要而右值引用可以对右值进行修改。
为什么
为了支持移动语义右值引用可以绑定到临时对象、表达式等右值上这些右值在生命周期结束后就会被销毁因此可以在右值引用中窃取其资源从而避免昂贵的复制操作实现高效的移动语义。完美转发右值引用可以绑定到任何类型的右值上可以将其作为参数传递给函数并在函数内部将其转发到其他函数中从而实现完美转发。扩展可变参数模板实现更加灵活的模板编程。 #include iostream// 定义一个类模拟资源管理
class Resource {
public:Resource() {std::cout Resource acquired std::endl;}~Resource() {std::cout Resource released std::endl;}void doSomething() {std::cout Doing something with the resource std::endl;}
};// 实现一个函数接受并修改临时对象
void modifyResource(Resource res) {std::cout Modifying the resource std::endl;res.doSomething(); // 对临时对象进行操作
}int main() {// 创建一个临时对象并将其传递给函数modifyResource(Resource()); // 使用临时对象调用函数return 0;
}
完美转发的解释 右值引用右值引用主要被用在移动语义和完美转发中。它由两个字符表示。当我们看到 Foo 那对应的就是右值引用。
完美转发这是C11特性可以让我们准确地将参数类型转发给另一个函数。例如我们有一个函数模版它接收一个参数。我们希望在函数模版内部根据这个参数是左值还是右值调用不同的函数或函数重载。完美转发就是这样的功能。
这里有一个更简单的例子我们有两个函数一个处理字符串另一个处理数字。
void process(int num) {std::cout Number: num std::endl;
}
void process(std::string str) {std::cout String: str std::endl;
} 我们想写一个函数模版它可以接受任何类型的参数并将其完美转发给 Process 函数。因此我们可以使用右值引用 和 std::forward。
template typename T
void Forward(T arg) {process(std::forwardT(arg));
} 现在我们可以在 Forward 函数中转发字符串和数字。
std::string s hello;
Forward(s); // 输出 String: hello
Forward(52); // 输出 Number: 52 在这个例子中Forward 函数通过使用右值引用 和 std::forward能够将参数完美地转发给 process 函数。不论传递给 Forward 的参数是什么类型的值std::forward 都能保证它的类型不变地被转发给 process 函数。
17. 左值引用和指针的区别
是否初始化指针可以不用初始化引用必须初始化。性质不同指针是一个变量引用是对被引用的对象取一个别名。占用内存单元不同指针有自己的空间地址引用和被引用对象占用同一个空间。 cpp
int main(){int x 10;int y 20;// 左值引用int ref x;ref 30; // x变为30// ref y; // 错误不能重新将引用绑定到其他变量std::cout x: x std::endl; // 输出x: 30// 指针int* ptr x;*ptr 40; // x变为40ptr y; // 可以重新将指针指向其他变量*ptr 50; // y变为50std::cout x: x , y: y std::endl; // 输出: x: 40, y: 50return 0;
}18. 指针是什么
指针全名是指针变量计算机在存储数据是有序存放的为了能够使用存放的地址就需要一个地址来区别每个数据的位置指针变量就是用来存放这些地址的变量。
解释 指针是一个变量其值为另一个变量的内存地址。你可以把它想象成一个可以找到存储在内存某处的数据的标签或者指示器。
在很多编程语言尤其是低级语言例如 C 或者 C 中指针有着非常重要的作用。使用指针我们可以直接访问和操作内存这在进行某些任务如动态内存分配传递复杂的数据结构等等时是非常有用的。
下面是一些基本的关于指针的特点
指针存储着变量的地址这是指针的核心特点和定义。指针的值本质上就是另一个变量的内存地址。
int x 5;
int *p x; // p 存储的就是 x 的内存地址。你可以通过指针访问和修改其指向的变量的值这就是所谓的 “解引用” 指针。在 C 和 C 中你可以用 * 操作符来解引用一个指针。
int x 5;
int *p x; // p now contains the address of x.
*p 10; // The value at the address stored in p is now 10. So, x is now 10.指针可以使你写出更高效的代码如果你正确使用指针例如来传递数据你可以减少程序的内存使用并提高它的效率。这是因为你可以直接传递大型数据结构的引用而不是复制整个数据结构。 尽管指针是一个非常强大并有用的工具但如果使用不正确它也可能会导致一些错误和问题如空指针解引用野指针等问题。因此在使用时一定要小心。
19. weak_ptr真的不计数是否有计数方式在哪分配的空间。
计数控制块中有强弱引用计数如果是使用make_shared初始化的函数则他所在的控制块空间是在所引用的shared_ptr中同一块空间若是new则控制器所分配的内存与shared_ptr本身所在的空间不在同一块内存。
20. 智能指针
C中的智能指针首先出现在“准”标准库boost中。随着使用的人越来越多为了让开发人员更方便、更安全的使用动态内存C11也引入了智能指针来管理动态对象。在新标准中主要提供了shared_ptr、unique_ptr、weak_ptr三种不同类型的智能指针。接下来的几篇文章我们就来总结一下这些智能指针的使用。
shared_ptr
shared_ptr是一个引用计数智能指针用于共享对象的所有权。也就是说它允许多个指针指向同一个对象。这一点与原始指针一致。 #include iostream
#include memory
using namespace std;class Example
{
public:Example() : e(1) { cout Example Constructor... endl; }~Example() { cout Example Destructor... endl; }int e;
};int main() {shared_ptrExample pInt(new Example());cout (*pInt).e endl;cout pInt引用计数: pInt.use_count() endl;shared_ptrExample pInt2 pInt;cout pInt引用计数: pInt.use_count() endl;cout pInt2引用计数: pInt2.use_count() endl;
}
/*
Example Constructor...
pInt: 1
pInt引用计数: 1
pInt引用计数: 2
pInt2引用计数: 2
Example Destructor...
*/
一方面跟STL中大多数容器类型一样shared_ptr也是模板类因此在创建shared_ptr时需要指定其指向的类型。另一方面正如其名一样shared_ptr指针允许让多个该类型的指针共享同一堆分配对象。同时shared_ptr使用经典的“引用计数”方法来管理对象资源每个shared_ptr对象关联一个共享的引用计数。
对于shared_ptr在拷贝和赋值时的行为《CPrimer第五版》中有详细的描述
每个shared_ptr都有一个关联的计数值通常称为引用计数。无论何时我们拷贝一个shared_ptr计数器都会递增。例如当用一个shared_ptr初始化另一个shred_ptr或将它当做参数传递给一个函数以及作为函数的返回值时它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁例如一个局部的shared_ptr离开其作用域时计数器就会递减。一旦一个shared_ptr的计数器变为0它就会自动释放自己所管理的对象。
对比我们上面的代码可以看到当我们将一个指向Example对象的指针交给pInt管理后其关联的引用计数为1。接下来我们用pInt初始化pInt2两者关联的引用计数值增加为2。随后函数结束pInt和PInt2相继离开函数作用于相应的引用计数值分别自减1最后变为0于是Example对象被自动释放调用其析构函数。
1. 创建shared_ptr实例
最安全和高效的方法是调用make_shared库函数该函数会在堆中分配一个对象并初始化最后返回指向此对象的share_ptr实例。如果你不想使用make_ptr也可以先明确new出一个对象然后把其原始指针传递给share_ptr的构造函数。 int main() {// 传递给make_shared函数的参数必须和shared_ptr所指向类型的某个构造函数相匹配shared_ptrstring pStr make_sharedstring(10, a);cout *pStr endl; // aaaaaaaaaaint *p new int(5);shared_ptrint pInt(p);cout *pInt endl; // 5
}
2. 访问所指对象
shared_ptr的使用方式与普通指针的使用方式类似既可以使用解引用操作符* 获得原始对象而进行访问其各个成员也可以使用指针访问符-来访问原始对象的各个成员。
3. 拷贝和赋值操作
我们可以用一个shared_ptr对象来初始化另一个shared_ptr实例该操作会增加其引用数值。
4、检查引用计数
shared_ptr提供了两个函数来检查其共享的引用计数值分别是unique()和use_count()。
在前面我们已经多次使用过use_count()函数该函数返回当前指针的引用计数值。值得注意的是use_count()函数可能效率很低应该只把它用于测试或调试。
unique()函数用来测试该shared_ptr是否是原始指针唯一拥有者也就是use_count()的返回值为1时返回true否则返回false。 int main() {shared_ptrstring pStr make_sharedstring(10, a);cout pStr.unique() endl; // trueshared_ptrstring pStr2(pStr);cout pStr2.unique() endl; // false;
}
weak_ptr
1. 为什么需要weak_ptr
shared_ptr是采用引用计数的智能指针多个shared_ptr实例可以指向同一个动态对象并维护了一个共享的引用计数器。对于引用计数法实现的计数总是避免不了循环引用或环形引用的问题shared_ptr也不例外。 #include iostream
#include memory
#include vector
using namespace std;class ClassB;class ClassA
{
public:ClassA() { cout ClassA Constructor... endl; }~ClassA() { cout ClassA Destructor... endl; }shared_ptrClassB pb; // 在A中引用B
};class ClassB
{
public:ClassB() { cout ClassB Constructor... endl; }~ClassB() { cout ClassB Destructor... endl; }shared_ptrClassA pa; // 在B中引用A
};int main() {shared_ptrClassA spa make_sharedClassA();shared_ptrClassB spb make_sharedClassB();spa-pb spb;spb-pa spa;// 函数结束思考一下spa和spb会释放资源么
}/*
ClassA Constructor...
ClassB Constructor...
Program ended with exit code: 0
*/ 可以看到循环引用spa和spb管理的动态资源并没有得到释放产生了内存泄露。为了解决类似这样的问题C11引入了weak_ptr来打破这种循环引用。
2. weak_ptr是什么
weak_ptr是为了配合shared_ptr而引入的一种智能指针它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向一旦最后一个指向对象的shared_ptr被销毁对象就会被释放。从这个角度看weak_ptr更像是shared_ptr的一个助手而不是智能指针。
3. weak_ptr的使用
初始化当我们创建一个weak_ptr时需要用一个shared_ptr实例来初始化weak_ptr由于是弱共享weak_ptr的创建并不会影响shared_ptr的引用计数值。 int main() {shared_ptrint sp(new int(5));cout 创建前sp的引用计数 sp.use_count() endl; // use_count 1weak_ptrint wp(sp);cout 创建后sp的引用计数 sp.use_count() endl; // use_count 1
}
如何判断weak_ptr指向对象是否存在
既然weak_ptr并不改变其所共享的shared_ptr实例的引用计数那就可能存在weak_ptr指向的对象被释放掉这种情况。这时我们就不能使用weak_ptr直接访问对象。那么我们如何判断weak_ptr指向对象是否存在呢C中提供了lock函数来实现该功能。 如果对象存在lock()函数返回一个指向共享对象的shared_ptr否则返回一个空shared_ptr。 class A
{
public:A() : a(3) { cout A Constructor... endl; }~A() { cout A Destructor... endl; }int a;
};int main() {shared_ptrA sp(new A());weak_ptrA wp(sp);//sp.reset();if (shared_ptrA pa wp.lock()){cout pa-a endl;}else{cout wp指向对象为空 endl;}
}
除此之外weak_ptr还提供了expired()函数来判断所指对象是否已经被销毁。 class A
{
public:A() : a(3) { cout A Constructor... endl; }~A() { cout A Destructor... endl; }int a;
};int main() {shared_ptrA sp(new A());weak_ptrA wp(sp);sp.reset(); // 此时sp被销毁cout wp.expired() endl; // true表示已被销毁否则为false
}
如何使用weak_ptr
weak_ptr并没有重载operator-和operator * 操作符因此不可直接通过weak_ptr使用对象典型的用法是调用其lock函数来获得shared_ptr示例进而访问原始对象。
最后我们来看看如何使用weak_ptr来改造最前面的代码打破循环引用问题。 class ClassB;class ClassA
{
public:ClassA() { cout ClassA Constructor... endl; }~ClassA() { cout ClassA Destructor... endl; }weak_ptrClassB pb; // 在A中引用B
};class ClassB
{
public:ClassB() { cout ClassB Constructor... endl; }~ClassB() { cout ClassB Destructor... endl; }weak_ptrClassA pa; // 在B中引用A
};int main() {shared_ptrClassA spa make_sharedClassA();shared_ptrClassB spb make_sharedClassB();spa-pb spb;spb-pa spa;// 函数结束思考一下spa和spb会释放资源么
}