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数据占用32bit,double数据占用64bit。我们在声明一个变量float f = 2.5的时候,无论是单精度还是双精度在存储中都分为三个部分:
- 符号位(Sign):0代表正,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-4,b的地址是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 函数,参数类型为 int
print(3.14); // 调用第二个 print 函数,参数类型为 double
return 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 = □
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_ptr<int> ptr(new int(42));
// 访问动态分配的内存
std::cout << "Value stored in dynamic memory: " << *ptr << std::endl;
// 当 unique_ptr 超出作用域时,它会自动释放所管理的内存
// 不需要手动调用 delete
return 0;
}
//在这个示例中,我们使用 `std::unique_ptr` 来管理动态分配的整数内存。在创建 `std::unique_ptr` 对象 `ptr` 时,我们将 `new int(42)` 返回的指针传递给了它。`std::unique_ptr` 对象负责管理这块内存,当 `ptr` 超出作用域时,它会自动调用 `delete` 来释放所管理的内存,从而避免了内存泄漏的风险。
//这就是智能指针的作用:通过对象管理资源,利用析构函数自动释放资源,提高了程序的安全性和可靠性。
//析构函数是在对象被销毁时自动调用的特殊成员函数。它的名称由波浪号(~)加上类名构成,没有返回类型,也不接受任何参数。析构函数通常用于在对象被销毁之前执行一些清理工作,比如释放动态分配的内存、关闭文件、释放资源等。
//当对象超出其作用域、被删除或者程序结束时,其析构函数会被调用。对于动态分配的对象(使用 `new` 关键字创建的对象),当调用 `delete` 来释放这些对象时,其析构函数也会被调用。 %%
#include <iostream>
class 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 <iostream>
int 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 <algorithm>
int main() {
// 定义一个存储匿名函数的容器
std::vector<std::function<int(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&&
,那对应的就是右值引用。
完美转发:这是C++11特性,可以让我们准确地将参数类型转发给另一个函数。例如,我们有一个函数模版它接收一个参数。我们希望在函数模版内部,根据这个参数是左值还是右值,调用不同的函数(或函数重载)。完美转发就是这样的功能。
这里有一个更简单的例子,我们有两个函数,一个处理字符串,另一个处理数字。
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::forward<T>(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变为40
ptr = &y; // 可以重新将指针指向其他变量
*ptr = 50; // y变为50
std::cout << "x: " << x << ", y: " << y << std::endl; // 输出: x: 40, y: 50
return 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中。随着使用的人越来越多,为了让开发人员更方便、更安全的使用动态内存,C++11也引入了智能指针来管理动态对象。在新标准中,主要提供了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_ptr<Example> pInt(new Example());
cout << (*pInt).e << endl;
cout << "pInt引用计数: " << pInt.use_count() << endl;
shared_ptr<Example> 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在拷贝和赋值时的行为,《C++Primer第五版》中有详细的描述:
每个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_ptr<string> pStr = make_shared<string>(10, 'a');
cout << *pStr << endl; // aaaaaaaaaa
int *p = new int(5);
shared_ptr<int> 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_ptr<string> pStr = make_shared<string>(10, 'a');
cout << pStr.unique() << endl; // true
shared_ptr<string> 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_ptr<ClassB> pb; // 在A中引用B
};
class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
shared_ptr<ClassA> pa; // 在B中引用A
};
int main() {
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
spa->pb = spb;
spb->pa = spa;
// 函数结束,思考一下:spa和spb会释放资源么?
}
/*
ClassA Constructor...
ClassB Constructor...
Program ended with exit code: 0
*/
可以看到,循环引用,spa和spb管理的动态资源并没有得到释放,产生了内存泄露。为了解决类似这样的问题,C++11引入了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_ptr<int> sp(new int(5));
cout << "创建前sp的引用计数:" << sp.use_count() << endl; // use_count = 1
weak_ptr<int> 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_ptr<A> sp(new A());
weak_ptr<A> wp(sp);
//sp.reset();
if (shared_ptr<A> 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_ptr<A> sp(new A());
weak_ptr<A> 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_ptr<ClassB> pb; // 在A中引用B
};
class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
weak_ptr<ClassA> pa; // 在B中引用A
};
int main() {
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
spa->pb = spb;
spb->pa = spa;
// 函数结束,思考一下:spa和spb会释放资源么?
}