文章目录
C++内存管理
堆和栈的区别
栈和堆都是⽤于存储程序数据的内存区域。栈是⼀种有限的内存区域,⽤于存储局部变量、函数调⽤信息等。堆是
⼀种动态分配的内存区域,⽤于存储程序运⾏时动态分配的数据。
栈上的变量⽣命周期与其所在函数的执⾏周期相同,⽽堆上的变量⽣命周期由程序员显式控制,可以(使⽤ new
或 malloc
)和释放(使⽤ delete
或 free
)。
栈上的内存分配和释放是⾃动的,速度较快。⽽堆上的内存分配和释放需要⼿动操作,速度相对较慢。
C++内存分区
C++程序运⾏时,内存被分为⼏个不同的区域,每个区域负责不同的任务。
- 栈
栈⽤于存储函数的局部变量、函数参数和函数调⽤信息的区域。函数的调⽤和返回通过栈来管理。 - 堆
堆⽤于存储动态分配的内存的区域,由程序员⼿动分配和释放。使⽤new
和delete
或malloc
和free
来进
⾏堆内存的分配和释放。 - 全局/静态区
全局区存储全局变量和静态变量。⽣命周期是整个程序运⾏期间。在程序启动时分配,程序结束时释放。 - 常ᰁ区
常ᰁ区也被称为只读区。存储常ᰁ数据,如字符串常ᰁ。 - 代码区
存储程序的代码。
内存泄漏?如何避免?
1、什么是内存泄露?
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使⽤的内存的情况。内存泄漏并⾮指内存
在物理上的消失,⽽是应⽤程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因⽽造成了内存的浪
费。
可以使⽤Valgrind, mtrace进⾏内存泄漏检查。
2、内存泄漏的分类
(1)堆内存泄漏 (Heap leak)
对内存指的是程序运⾏中根据需要分配通过malloc,realloc new等从堆中分配的⼀块内存,再是完成后必须通过调
⽤对应的 free或者 delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被
使⽤,就会产⽣ Heap Leak.
(2)系统资源泄露(Resource Leak)
主要指程序使⽤系统分配的资源⽐如 Bitmap,handle ,SOCKET 等没有使⽤相应的函数释放掉,导致系统资源的浪
费,严᯿可导致系统效能降低,系统运⾏不稳定。
(3)没有将基类的析构函数定义为虚函数
当基类指针指向⼦类对象时,如果基类的析构函数不是 virtual,那么⼦类的析构函数将不会被调⽤,⼦类的资源没
有正确是释放,因此造成内存泄露。
3、什么操作会导致内存泄露?
指针指向改变,未释放动态分配内存。
4、如何防⽌内存泄露?
将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;使⽤智能指针
5、智能指针有了解哪些?
智能指针是为了解决动态分配内存导致内存泄露和多次释放同⼀内存所提出的,C11标准中放在< memory>头⽂
件。包括:共享指针,独占指针,弱指针
6、构造函数,析构函数要设为虚函数吗,为什么?
(1)析构函数
析构函数需要。当派⽣类对象中有内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调⽤基类
析构函数,导致派⽣类资源⽆法释放,造成内存泄漏。
(2)构造函数
构造函数不需要,没有意义。虚函数调⽤是在部分信息下完成⼯作的机制,允许我们只知道接⼝⽽不知道对象的确
切类型。 要创建⼀个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构
造函数不应该被定义为虚函数。
什么是智能指针?有哪些种类?
智能指针⽤于管理动态内存的对象,其主要⽬的是在避免内存泄漏和⽅便资源管理。
std::unique_ptr
独占智能指针
std::unique_ptr
提供对动态分配的单⼀对象所有权的独占管理。通过独占所有权,确保只有⼀个
std::unique_ptr
可以拥有指定的内存资源。移动语义和右值引⽤允许std::unique_ptr
在所有权转移时⾼效地进⾏转移。
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::shared_ptr
(共享智能指针):
std::shared_ptr
允许多个智能指针共享同⼀块内存资源。内部使⽤引⽤计数来跟踪对象被共享的次数,当计数为零时,资源被释放。提供更灵活的内存共享,但可能存在循环引⽤的问题。
#include <memory>
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1;
std::weak_ptr
(弱引⽤智能指针):
std::weak_ptr
⽤于解决std::shared_ptr
可能导致的循环引⽤问题。
std::weak_ptr
可以从std::shared_ptr
创建,但不会增加引⽤计数,不会影响资源的释放。
通过std::weak_ptr::lock()
可以获取⼀个std::shared_ptr
来访问资源。
#include <memory>
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;
new 和 malloc 有什么区别?
类型安全性:
new
是C++的运算符,可以为对象分配内存并调⽤相应的构造函数。malloc
是C语⾔库函数,只分配指定⼤⼩的内存块,不会调⽤构造函数。
返回类型:
new
返回的是具体类型的指针,⽽且不需要进⾏类型转换。malloc
返回的是void*
,需要进⾏类型转换,因为它不知道所分配内存的⽤途。
内存分配失败时的⾏为:
new
在内存分配失败时会抛出std::bad_alloc
异常。malloc
在内存分配失败时返回NULL
。
内存块⼤⼩:
new
可以⽤于动态分配数组,并知道数组⼤⼩。malloc
只是分配指定⼤⼩的内存块,不了解所分配内存块的具体⽤途。
释放内存的⽅式:
delete
会调⽤对象的析构函数,然后释放内存。free
只是简单地释放内存块,不会调⽤对象的析构函数。
delete 和 free 有什么区别?
类型安全性:
delete
会调⽤对象的析构函数,确保资源被正确释放。free
不了解对象的构造和析构,只是简单地释放内存块。
内存块释放后的⾏为:delete
释放的内存块的指针值会被设置为 nullptr ,以避免野指针。free
不会修改指针的值,可能导致野指针问题。
数组的释放:delete
可以正确释放通过new[]
分配的数组。
free
不了解数组的⼤⼩,不适⽤于释放通过malloc
分配的数组。
什么是野指针,怎么产⽣的,如何避免
野指针是指指向已被释放的或⽆效的内存地址的指针。使⽤野指针可能导致程序崩溃、数据损坏或其他不可预测的
⾏为。通常由以下⼏种情况产⽣
- 释放后没有置空指针
int* ptr = new int;
delete ptr;
// 此时 ptr 成为野指针,因为它仍然指向已经被释放的内存
ptr = nullptr; // 避免野指针,应该将指针置为 nullptr 或赋予新的有效地址
- 返回局部变野的指针
int* createInt() {
int x = 10;
return &x; // x 是局部变量,函数结束后 x 被销毁,返回的指针成为ᰀ指针
}
// 在使⽤返回值时可能引发未定义⾏为
- 释放内存后没有调整指针
int* ptr = new int;
// 使⽤ ptr 操作内存
delete ptr;
// 此时 ptr 没有被置为 nullptr 或新的有效地址,成为野指针
// 避免:delete ptr; ptr = nullptr;
- 函数参数指针被释放
void foo(int* ptr) {
// 操作 ptr
delete ptr;
}
int main() {
int* ptr = new int;
foo(ptr);
// 在 foo 函数中 ptr 被释放,但在 main 函数中仍然可⽤,成为野指针
// 避免:在 foo 函数中不要释放调⽤⽅传递的指针
}
如何避免野指针
- 在释放内存后将指针置为
nullptr
int* ptr = new int;
// 使⽤ ptr 操作内存
delete ptr;
ptr = nullptr; // 避免成为ᰀ指针
- 避免返回局部变量的指针
int* createInt() {
int* x = new int;
*x = 10;
return x;
}
- 使⽤智能指针(如
std::unique_ptr
和std::shared_ptr
):
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// 使⽤ std::unique_ptr,避免显式 delete,指针会在超出作⽤域时⾃动释放
- 注意函数参数的⽣命周期, 避免在函数内释放调⽤⽅传递的指针,或者通过引⽤传递指针。
void foo(int*& ptr) {
// 操作 ptr
delete ptr; // 这⾥可能造成调⽤⽅的指针成为ᰀ指针
ptr = nullptr;
}
int main() {
int* ptr = new int;
foo(ptr);
}
野指针和悬浮指针的区别
野指针是指向已经被释放或者⽆效的内存地址的指针。通常由于指针指向的内存被释放,但指针本身没有被置为
nullptr
或者重新分配有效的内存,导致指针仍然包含之前的内存地址。使⽤ᰀ指针进⾏访问会导致未定义⾏
为,可能引发程序崩溃、数据损坏等问题。
悬浮指针是指向已经被销毁的对象的引⽤。当函数返回⼀个局部变ᰁ的引⽤,⽽调⽤者使⽤该引⽤时,就可能产⽣
悬浮引⽤。访问悬浮引⽤会导致未定义⾏为,因为引⽤指向的对象已经被销毁,数据不再有效。
区别:
关联对象类型:
- 野指针涉及指针类型。
- 悬浮指针涉及引⽤类型。
问题表现: - 野指针可能导致访问已释放或⽆效内存,引发崩溃或数据损坏。
- 悬浮指针可能导致访问已销毁的对象,引发未定义⾏为。
产⽣原因: - 野指针通常由于不正确管理指针⽣命周期引起。
- 悬浮指针通常由于在函数中返回局部变ᰁ的引⽤引起。
如何避免悬浮指针 - 避免在函数中返回局部变ᰁ的引⽤。
- 使⽤返回指针或智能指针⽽不是引⽤,如果需要在函数之外使⽤函数内部创建的对象。
内存对齐是什么?为什么需要考虑内存对齐?
1、什么是内存对齐
内存对齐是指数据在内存中的存储起始地址是某个值的倍数。
在C语⾔中,结构体是⼀种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是⼀些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其⾃然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第⼀个成员的地址和整个结构体的地址相同。
为了使CPU能够对变ᰁ进⾏快速的访问,变ᰁ的起始地址应该具有某些特性,即所谓的“对⻬”,⽐如4字节的int
型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对⻬”跟数据在内存中的位置有关。如果⼀个变ᰁ的内存地址正好位于它⻓度的整数倍,他就被称做⾃然对⻬。
⽐如在32位cpu下,假设⼀个整型变量的地址为0x00000004(为4的倍数),那它就是⾃然对⻬的,⽽如果其地址为0x00000002(⾮4的倍数)则是⾮对⻬的。现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照⼀定的规则在空间上排列,⽽不是顺序的⼀个接⼀个的排放,这就是对⻬。
2、为什么需要考虑内存对⻬
需要字节对⻬的根本原因在于CPU访问数据的效率问题。假设上⾯整型变量的地址不是⾃然对⻬,⽐如为
0x00000002,则CPU如果取它的值的话需要访问两次内存,第⼀次取从0x00000002-0x00000003的⼀个short,第⼆次取从0x00000004-0x00000005的⼀个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第⼀次为char,第⼆次为short,第三次为char,然后组合得到整型数据。
⽽如果变量在⾃然对⻬位置上,则只要⼀次就可以取出数据。⼀些系统对对⻬要求⾮常严格,⽐如sparc系统,如
果取未对⻬的数据会发⽣错误,⽽在x86上就不会出现错误,只是效率下降。
各个硬件平台对存储空间的处理上有很⼤的不同。⼀些平台对某些特定类型的数据只能从某些特定地址开始存取。⽐如有些平台每次读都是从偶地址开始,如果⼀个int型(假设为32位系统)如果存放在偶地址开始的地⽅,那么⼀个读周期就可以读出这32bit,⽽如果存放在奇地址开始的地⽅,就需要2个读周期,并对两次读出的结果的⾼低字节进⾏拼凑才能得到该32bit数据。显然在读取效率上下降很多。
- ⼤多数计算机硬件要求基本数据类型的变ᰁ在内存中的地址是它们⼤⼩的倍数。例如,⼀个 32 位整数通常需要在内存中对⻬到 4 字节边界。
- 内存对⻬可以提⾼访问内存的速度。当数据按照硬件要求的对⻬⽅式存储时,CPU可以更⾼效地访问内存,减少因为不对⻬⽽引起的性能损失。
- 许多计算机体系结构使⽤缓存⾏(cache line)来从内存中加载数据到缓存中。如果数据是对⻬的,那么⼀个缓存⾏可以装载更多的数据,提⾼缓存的命中率。
- 有些计算机架构要求原⼦性操作(⽐如原⼦性读写)必须在特定的内存地址上执⾏。如果数据不对⻬,可能导
致⽆法执⾏原⼦性操作,进⽽引发竞态条件。
测试题⽬
1、以下为WindowsNT 32位C++程序,请计算下⾯sizeof的值
char str[] = "hello";
char* p = str;
int n = 10;
// 请计算
sizeof(str) = ?
sizeof(p) = ?
sizeof(n) = ?
void Func(char str[100])
{
// 请计算
sizeof(str) = ?
}
void* p = malloc(100);
// 请计算
sizeof(p) = ?
参考答案:
sizeof(str) = 6;
sizeof()计算的是数组的所占内存的⼤⼩包括末尾的 ‘\0’
sizeof(p) = 4;
p为指针变ᰁ,32位系统下⼤⼩为 4 bytes
sizeof(n) = 4;
n 是整型变ᰁ,占⽤内存空间4个字节
void Func(char str[100])
{
sizeof(str) = 4;
}
函数的参数为字符数组名,即数组⾸元素的地址,⼤⼩为指针的⼤⼩
void* p = malloc(100);
sizeof(p) = 4;
p指向malloc分配的⼤⼩为100 byte的内存的起始地址,sizeof§为指针的⼤⼩,⽽不是它指向内存的⼤⼩
2、分析运⾏下⾯的Test函数会有什么样的结果
void GetMemory1(char* p)
{
p = (char*)malloc(100);
}
void Test1(void)
{
char* str = NULL;
GetMemory1(str);
strcpy(str, "hello world");
printf(str);
}
char *GetMemory2(void)
{
char p[] = "hello world";
return p;
}
void Test2(void)
{
char *str = NULL;
str = GetMemory2();
printf(str);
}
void GetMemory3(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test3(void)
{
char* str = NULL;
GetMemory3(&str, 100);
strcpy(str, "hello");
printf(str);
}
void Test4(void)
{
char *str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL) {
strcpy(str, "world");
cout << str << endl;
}
}
参考答案:
Test1(void):
程序崩溃。 因为GetMemory1并不能传递动态内存,Test1函数中的 str⼀直都是NULL。strcpy(str, “hello world”)
将使程序奔溃
Test2(void):
可能是乱码。 因为GetMemory2返回的是指向“栈内存”的指针,该指针的地址不是NULL,使其原现的内容已经被
清除,新内容不可知。
Test3(void):
能够输出hello, 内存泄露。GetMemory3申请的内存没有释放
Test4(void):
篡改动态内存区的内容,后果难以预料。⾮常危险。因为 free(str);之后,str成为野指针,if(str != NULL)语句不
起作⽤。
3、实现内存拷⻉函数
char* strcpy(char* strDest, const char* strSrc);
}
参考答案:(函数实现)
char* strcpy(char *dst,const char *src) {// [1]
assert(dst != NULL && src != NULL); // [2]
char *ret = dst; // [3]
while ((*dst++=*src++)!='\0'); // [4]
return ret;
}
[1] const修饰:
(1)源字符串参数⽤const修饰,防⽌修改源字符串。
[2] 空指针检查:
(1)不检查指针的有效性,说明答题者不注᯿代码的健壮性。
(2)检查指针的有效性时使⽤ assert(!dst && !src);
char *转换为 bool 即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增⼤和维护成本升⾼。
(3)检查指针的有效性时使⽤ assert(dst != 0 && src != 0);
直接使⽤常ᰁ(如本例中的0)会减少程序的可维护性。⽽使⽤NULL代替0,如果出现拼写错误,编译器就会检查
出来。
[3] 返回⽬标地址:
(1)忘记保存原始的strdst值。
[4] ‘\0’:
(1)循环写成 while (*dst++=*src++);
明显是错误的。
(2)循环写成 while (*src!='\0') *dst++ = *src++;
循环体结束后,dst字符串的末尾没有正确地加上’\0’。
(3)为什么要返回char *?
返回dst的原始值使函数能够⽀持链式表达式
链式表达式的形式如:
int l=strlen(strcpy(strA,strB));
⼜如:
char * strA=strcpy(new char[10],strB);
返回strSrc的原始值是错误的。
理由:
- 源字符串肯定是已知的,返回它没有意义
- 不能⽀持形如第⼆例的表达式
- 把 const char *作为char * 返回,类型不符,编译报错
4、假如考虑dst和src内存᯿叠的情况,strcpy该怎么实现
char s[10]="hello";
strcpy(s, s+1);
// 应返回 ello
strcpy(s+1, s);
// 应返回 hhello 但实际会报错
// 因为dst与src᯿叠了,把'\0'覆盖了
所谓重叠,就是src未处理的部分已经被dst给覆盖了,只有⼀种情况: src<=dst<=src+strlen(src)
C函数 memcpy ⾃带内存᯿叠检测功能,下⾯给出 memcpy 的实现my_memcpy
char * strcpy(char *dst,const char *src)
{
assert(dst != NULL && src != NULL);
char *ret = dst;
my_memcpy(dst, src, strlen(src)+1);
return ret;
}
/* my_memcpy的实现如下 */
char *my_memcpy(char *dst, const char* src, int cnt)
{
assert(dst != NULL && src != NULL);
char *ret = dst;
/*内存重叠,从⾼地址开始复制*/
if (dst >= src && dst <= src+cnt-1)
{
dst = dst+cnt-1;
src = src+cnt-1;
while (cnt--)
{
*dst-- = *src--;
}
}
else //正常情况,从低地址开始复制
{
while (cnt--)
{
*dst++ = *src++;
}
}
return ret;
}
5、按照下⾯描述的要求写程序
已知String的原型为:
class String
{
public:
String(const char *str = NULL);
String(const String &other);
~ String(void);
String & operate =(const String &other);
private:
char *m_data;
};
请编写上述四个函数
参考答案:
此题考察对构造函数赋值运算符实现的理解。实际考察类内含有指针的构造函数赋值运算符函数写法。
// 构造函数
String::String(const char *str)
{
if(str==NULL)
{
m_data = new char[1]; //对空字符串⾃动申请存放结束标志'\0'
*m_data = '\0';
}
else
{
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}
// 析构函数
String::~String(void)
{
delete [] m_data; // 或delete m_data;
}
//拷⻉构造函数
String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
}
//赋值函数
String &String::operate =(const String &other)
{
if(this == &other)
{
return *this; // 检查⾃赋值
}
delete []m_data; // 释放原有的内存资源
int length = strlen(other.m_data);
m_data = new char[length + 1]; //对m_data加NULL判断
strcpy(m_data, other.m_data);
return *this; //返回本对象的引⽤
}
6、说⼀说进程的地址空间分布
参考答案:
对于⼀个进程,其空间分布如下图所示:
如上图,从⾼地址到低地址,⼀个程序由命令⾏参数和环境变量、栈、⽂件映射区、堆、BSS段、数据段、代码段
组成。
(1)命令⾏参数和环境变量
命令⾏参数是指从命令⾏执⾏程序的时候,给程序的参数。
(2)栈区
存储局部变量、函数参数值。栈从⾼地址向低地址增⻓。是⼀块连续的空间。
(3)⽂件映射区
位于堆和栈之间。
(4)堆区
动态申请内存⽤。堆从低地址向⾼地址增⻓。
(5)BSS 段
存放程序中未初始化的 全局变量和静态变量的⼀块内存区域。
(6)数据段
存放程序中已初始化的 全局变量和静态变量的⼀块内存区域。
(7)代码段
存放程序执⾏代码的⼀块内存区域。只读,代码段的头部还会包含⼀些只读的常数变量。
7、说⼀说C与C++的内存分配⽅式
(1)从静态存储区域分配
内存在程序编译的时候就已经分配好,这块内存在程序的整个运⾏期间都存在,如全局变量,static变量。
(2)在栈上创建
在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存
分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。
(3)从堆上分配(动态内存分配)程序在运⾏的时候⽤malloc或new申请任意多少的内存,程序员负责在何时⽤free或delete释放内存。动态内存的⽣存期⾃⼰决定,使⽤⾮常灵活。
8、new、delete、malloc、free关系
参考答案:
如果是带有⾃定义析构函数的类类型,⽤ new [] 来创建类对象数组,⽽⽤ delete来释放会发⽣什么?⽤上⾯的例
⼦来说明:
class A {};
A* pAa = new A[3];
delete pAa;
那么 delete pAa; 做了两件事:
- 调⽤⼀次 pAa 指向的对象的析构函数
- 调⽤ operator delete(pAa);释放内存
显然,这⾥只对数组的第⼀个类对象调⽤了析构函数,后⾯的两个对象均没调⽤析构函数,如果类对象中申请了⼤
量的内存需要在析构函数中释放,⽽你却在销毁数组对象时少调⽤了析构函数,这会造成内存泄漏。
上⾯的问题你如果说没关系的话,那么第⼆点就是致命的了!直接释放pAa指向的内存空间,这个总是会造成严重
的 段错误,程序必然会奔溃!因为分配的空间的起始地址是 pAa 指向的地⽅减去 4 个字节的地⽅。你应该传⼊参
数设为那个地址!
计算机中的乱序执⾏
1、⼀定会按正常顺序执⾏的情况
- 对同⼀块内存进⾏访问,此时访问的顺序不会被编译器修改
- 新定义的变ᰁ的值依赖于之前定义的变量,此时两个变ᰁ定义的顺序不会被编译器修改
2、其他情况计算机会进⾏乱序执行
单线程的情况下允许,但是多线程情况下就会产⽣问题
3、C++中的库中提供了六种内存模型
⽤于在多线程的情况下防⽌编译器的乱序执⾏
(1)memory_order_relaxed
最放松的
(2)memory_order_consume
当客户使⽤,搭配release使⽤,被release进⾏赋值的变ᰁy,获取的时候如果写成consume,那么所有与y有关的
变ᰁ的赋值⼀定会被按顺序进⾏
(3)memory_order_acquire
⽤于获取资源
(4)memory_order_release
⼀般⽤于⽣产者,当给⼀个变ᰁy进⾏赋值的时候,只有⾃⼰将这个变ᰁ释放了,别⼈才可以去读,读的时候如果
使⽤acquire来读,编译器会保证在y之前被赋值的变ᰁ的赋值都在y之前被执⾏,相当于设置了内存屏障
(5)memory_order_acq_rel(acquire/release)
(6)memory_order_seq_cst(squentially consistent)
好处:不需要编译器设置内存屏障,morden c++开始就会有底层汇编的能⼒
副作⽤
1、⽆副作⽤编程
存在⼀个函数,传⼀个参数x进去,⾥⾯进⾏⼀系列的运算,返回⼀个y。中间的所有过程都是在栈中进⾏修改
2、有副作⽤编程
⽐如在⼀个函数运⾏的过程中对全局变ᰁ进⾏了修改或在屏幕上输出了⼀些东⻄。此函数还有可能是类的成员⽅法,在此⽅法中如果对成员变ᰁ进⾏了修改,类的状态就会发⽣改变
3、在多线程情况下的有副作⽤编程
在线程1运⾏的时候对成员变ᰁ进⾏了修改,此时如果再继续运⾏线程2,此时线程2拥有的就不是这个类的初始状态,运⾏出来的结果会收到线程1的影响
解决办法:将成员⽅法设为const,此时就可以放⼼进⾏调⽤
信号量
1、binary_semaphore
定义:
可以当事件来⽤,只有有信号和⽆信号两种状态,⼀次只能被⼀个线程所持有。
使⽤步骤:
(1)初始创建信号ᰁ,并且⼀开始将其置位成⽆信号状态
std::binary_semaphore sem(0)
(2)线程使⽤acquire()⽅法等待被唤醒
(3)主线程中使⽤release()⽅法,将信号ᰁ变成有信号状态
2、counting_semaphore
定义:
⼀次可以被很多线程所持有,线程的数ᰁ由⾃⼰指定
使⽤步骤:
(1)创建信号ᰁ
指定⼀次可以进⼊的线程的最⼤数ᰁ,并在最开始将其置位成⽆信号状态:std::biinary_semaphore<8> sem(0);
(2)主线程中创建10个线程
并且这些线程全部调⽤acquire()⽅法等待被唤醒。但是主线程使⽤release(6)⽅法就只能随机启⽤6个线程。
future库
⽤于任务链(即任务A的执⾏必须依赖于任务B的返回值)
1、例⼦:⽣产者消费者问题
(1)⼦线程作为消费者
参数是⼀个future,⽤这个future等待⼀个int型的产品:std::future& fut
(2)⼦线程中使⽤get()⽅法等待⼀个未来的future,返回⼀个result
(3)主线程作为⽣产者,做出⼀个承诺:std::promise prom
(4)⽤此承诺中的get_future()⽅法获取⼀个future
(5)主线程中将⼦线程创建出来,并将刚刚获取到的future作为参数传⼊
(6)主线程做⼀些列的⽣产⼯作,最后⽣产完后使⽤承诺中的set_value()⽅法,参数为刚刚⽣产出的产品
(7)此时产品就会被传到⼦线程中,⼦线程就可以使⽤此产品做⼀系列动作
(8)最后使⽤join()⽅法等待⼦线程停⽌,但是join只适⽤于等待没有返回值的线程的情况
2、如果线程有返回值
(1)使⽤async⽅法可以进⾏异步执⾏
参数⼀: 可以选择是⻢上执⾏还是等⼀会执⾏(即当消费者线程调⽤get()⽅法时才开始执⾏)
参数⼆: 执⾏的内容(可以放⼀个函数对象或lambda表达式)
(2)⽣产者使⽤async⽅法做⽣产⼯作并返回⼀个future
(3)消费者使⽤future中的get()⽅法可以获取产品
字符串操作函数
常⻅的字符串函数实现
1、strcpy()
把从strsrc地址开始且含有’\0’结束符的字符串复制到以strdest开始的地址空间,返回值的类型为char*
char *strcpy(char *strDest, const char *strSrc)
{
assert((strDest != NULL) && (strSrc != NULL));
char *address = strDest;
while((*strDest++ = *strSrc++) != '\0';
return address;
}
2、strlen()
计算给定字符串的⻓度。
int strlen(const char *str)
{
assert(str != NULL);
int len;
while((*str++) != "\0")
{
len++;
}
return len;
}
3、strcat()
作⽤是把src所指字符串添加到dest结尾处。
char * strcat(char *dest, const char *src)
{
assert(dest && src);
char * ret = dest;
while(*dest)
{
dest++;
}
while(*dest++ = *src++){}
return ret;
}
4、strcmp()
⽐较两个字符串设这两个字符串为str1, str2,
若str1 == str2,则返回零
若str1 < str2,则返回负数
若str1 > str2,则返回正数
char * strcmp(const char *str1, const char *str2)
{
assert(str1 && str2);
while(*str1 && *str2 && (*str1==*str2))
{
str1++;
str2++
}
return *str1 - *str2;
}
标签:函数,C++,char,内存,str,ptr,指针
From: https://blog.csdn.net/weixin_62621696/article/details/141164867