C++基础知识与概念
- C++与C基础
- C++面向对象
- C++ STL
- C++ 内存管理
- C++11新特性
- C++ linux编程+I/O多路复用
前言
本文章适用于有一定C++基础的新手同学快速掌握一些C++的基本知识概念以及面试中可能会问的内容,如果你没有相应的基础学习又因为这篇文章缺少代码讲解所以可能会影响你的学习效率.内容出自编程书,网上资料以及chatgpt,其中的一些知识也只是我从网络上查来的资料并没有经过权威的认证,所以说很可能有错误,对我来说过难或者较为深入的地方因为本人水平有限没有给出过于详细的解释,望见谅.
C++与C基础
static关键字的作用
static关键字的作用就是可以让一个变量变成一个静态变量
1.加在一个全局变量前面就会变成一个全局静态变量,那么它就会被放入程序的静态存储区当中,程序存在
期间它会一直存在
2.加在一个局部变量前就会变成一个局部静态变量,它的作用域仍然是局部作用域,随着函数的结束它的作用域
也会结束,但是作用域结束并不会使它在内存中被销毁,反而会仍然存在于内存之中,当再次调用函数时
它的值不变
3.静态函数,在函数的返回值前加static,函数就会变成静态函数,表示声明该函数仅在该文件下是可见的,
对于其他文件不可见,所以静态函数不会和其他文件中的同名函数起冲突
4.加在类的成员变量前,就会使其变成类的静态成员变量,它的意义就是让所有该类的对象可以共享一些数据,
当你想访问类的静态成员变量时,只需要通过类名即可访问,而不需要通过对象名来访问.
5.加在类的成员函数前,就会使其变成类的静态成员函数,意义跟类的静态成员变量一样,
也是需要直接通过类名来调用,值得注意的是,在类的静态成员函数中,
只能访问类的静态变量而不能访问非静态成员变量.
C和C++的区别
在设计思想上
C++是面向对象的语言,C语言是面向过程的语言
正式因为C++面向对象,所以它拥有面向对象的三大特点:继承,封装,多态
除此之外C++还支持函数重载,更多方式的强制类型转换(static_cast,dynamic_cast,const_cast,
reinterpret_cast)
并且还支持范式编程,模板类,函数模板,以及容器和算法
指针和引用的区别
首先二者就完全不是一个东西
1.指针是有自己的空间的,而引用只是一个别名
2.指针是由自己固定的大小的,在32位操作系统上,指针大小为4字节,在64位操作系统上,指针大小为8字节
而引用大小是引用对象决定的
3.指针在定义的时候可以暂时不初始化,但是引用在定义的时候必须进行初始化
4.指针可以有多级指针,引用没有多级的概念
5.如果返回动态分配的对象或者内存,需要使用指针,使用引用可能会导致内存泄漏
...
C++强制类型转换
static_cast:
static_cast是一种静态型的强制类型转换,所谓静态就是它在编译期就可以确定好强制转换的结果
static可以用于:
1.基础类型之间的转换( 如int转char)
2.表达式转换成void类型
3.void*转换成其他类型的指针
4.左值到右值,数组到指针,函数到指针之间的转换
5.基类和派生类之前的向上转型,向上转型是安全的,向下转型是不安全的
向上转型:派生类转换成基类
向下转型:基类转换成派生类
dynamic_cast:
dynamic_cast是一种动态的强制类型转换,它的转换会在运行时进行类型检查,并且转换的条件相对
static_cast来说也非常苛刻
1.dynamic_cast不能用于基础类型的强制类型转换
2.dynamic_cast只能用于有虚函数的类,用于类层次之间的强制类型转换(基类和派生类之间),并且只能
用于指针和引用之间的强制类型转换
3.相比与static_cast来说,dynamic_cast不管是向上转型还是向下转型都是安全的
const_cast:
用于将常量转换成非常量
reinterpret_cast:
几乎什么都可以转,甚至可以把两个毫不相关的类型进行转换,但是是不安全的,可能出一些错误
malloc-free和new-delete的区别
1.malloc函数和free函数是C语言的库函数,它们存在与stdlib这个库之中
而new和delete是C++中的关键字
2.malloc函数和free函数在使用的时候,必须要指明开辟/释放的空间大小,而new和delete是根据对象类型
自动计算内存大小不显示指明
3.malloc函数它返回的是一个void类型的指针,所以它需要手动的进行类型转换,而new返回的是指定类型
的指针,是不需要手动进行类型转换的
4.malloc函数只负责分配内存而不会调用对象的构造函数,new关键字确保对象被正确初始化是会调用构造
函数的,同理delete函数也是调用对象的析构函数来释放空间的
ps:malloc原理在下面[c++内存管理]中讲解
野指针
野指针就是指向无效内存地址的指针
它指向的地址可能被回收或者不属于当前程序的内存空间
应对方法:每次对指针操作时确保指针指向的是一个正确的地址,或者指向nullptr
C语言的函数调用
C语言是依赖栈来进行函数调用的,在每次调用函数时,都会在栈上分配空间,在栈内进行函数的执行过程
每次执行函数前,都会将函数的执行位置,变量,函数的返回地址,函数的ESP指针等压入栈中,然后进行
函数传参,执行函数体内的内容,在函数结束时,它会把函数的返回值放入函数调用栈或者寄存器之中,然后
把之前存放入栈内的执行位置,变量等恢复,让程序回到执行函数前的位置,并把栈内存放的一些局部变量销毁
释放.
C语言函数的返回值存放在哪儿
1.函数的返回值通常被存放到【函数调用栈】之中
函数调用栈是一个用来管理函数调用的数据结构,每次调用函数时,它会创建一个【栈帧】放到函数
调用栈的顶部,栈帧之中存放函数调用的参数,局部变量,返回值等,函数结束时,返回值就被放在栈帧的
特殊位置里,调用方可以根据这个位置找到返回值并把它赋值给其他变量
2.对于一些比较小的返回值(整数,指针等)是被存放入寄存器中的,而非函数调用栈之中,这样可以
提高程序的运行效率,减小频繁的内存读写,但是因为寄存器存放返回值的能力有限,所以对于一些较
大的数据(类,结构体)是被存放在函数调用栈之中
3.所以具体返回值被存放在哪儿,取决于编译器的优化策略,和返回值的类型和大小
extern “C”
参考
如果你想在cpp文件中使用.c文件中定义的函数,你需要在C语言的.h文件合适的位置加上extern"C",或者
在cpp文件引入头文件的时候加上extern “C”
原理:因为C++支持函数重载,那么一个在C语言和C++中的同名函数经过编译之后的函数名其实是不一样的
如果在C++中直接调用.c文件中的函数,那么其实是在用C++的名称修饰方式进行函数的查找和链接,所以
就会发生错误,加上extern "C"之后,这部分代码就会按照C语言的方式编译和链接,所以就可以正常调用
头文件引入时 “”和<>的区别
主要区别是编译器查找头文件的路径顺序不一样
1.“”编译器首先会去当前文件所在的目录下去查找,然后再去编译器所设置的头文件路径下去查找
2.<>编译器首先会去系统的标准库目录下去查找,然后再去编译器所设置的头文件路径下去查找
一般的,我们用“”来引入自定义的头文件,用<>引入系统自带的头文件
函数指针
在C语言编译的过程中,每个函数都会有一个自己的返回地址,函数指针指向的就是这些返回地址,有了
指向函数的指针变量之后,我们就可以直接通过指针变量使用函数了
形参与实参
形参是函数的参数列表中那些没有确定值的函数参数
而实参是在函数调用时,传入形参中的实际的值
C++中两个同名函数一个带const一个不带会出问题吗
不会,相当于函数的重载
什么是C++的API
API的全称是应用程序编程接口(Application Programming Interface)它定义了软件组件之间交互
的规则和约定,允许不同软件应用程序之间进行交互.
C++的api:可以把它理解为库或者框架提供的接口,用于库或框架交互或访问其功能,在C++编程中
我们常常用api扩展和强化应用程序
1.标准库的api,标准库中有大量容器和算法,比如说vector容器中的函数push_back也算是一个小的api
sort排序算法也是api
2.第三方库api:Opencv库中的各种功能,Boost库中的各种高级功能,都是第三方库api
3.自定义api:我们可以在我们自己的代码中自定义类和函数,并提供api给其他使用者使用我们的代码,
还可以把类的公有成员函数作为api,让其他代码通过调用函数来访问和操作我们的类
使用API的方法
1.引入相应的头文件
2.创建对象和构造函数
3.处理返回值
如何处理内存泄漏
可以使用一些用于内存泄漏检测的工具
比如说 crtdbg库,它是 Microsoft Visual C++ 提供的一个扩展库。
用于检测和报告内存泄漏、访问越界、错误的内存分配等问题
_CrtDumpMemoryLeaks:在程序结束时检查内存泄漏,并将泄漏的内存信息输出到调试窗口。
一般的内存泄漏检测工具都有跟踪malloc函数和free函数的功能,可以统计这两个函数分配内存和释放
内存的情况,如果二者不一致,说明发生了内存泄漏.
内存泄漏的常见情况:
1.堆内存泄漏:动态分配的内存没有手动释放
2.系统资源泄漏:使用系统分配的资源,没有及时用相应的函数释放掉
比如使用SOCKET进行网络编程,没有使用close函数关闭
3.没有将基类中的析构函数定义为虚函数
什么情况会发生段错误
非法访问内存地址(数组越界)
1.使用野指针
2.修改常量的值也有可能会导致段错误
C++面向对象
面向对象
面向对象特点分别是:封装,继承,多态
1.封装:有两个作用,第一个作用是将许多小对象封装成一个大对象,第二个作用是可以将一些内部的
方法和属性对外界屏蔽,提高安全性
2.继承:继承就是让子类自动共享父类的数据结构和方法,提高程序的开放性,扩展性,最重要的是它可以
提高代码的复用性,降低程序员创建类,对象时的工作量
3.多态:多态主要体现在虚函数上,它通过虚函数的重写,可以让许多功能不同的函数共用一个函数名,
通过虚函数可以实现发送同一个消息给不同的对象时,会产生完全不同的行为,消息主要指函数的调用
不同的行为指的是不同的实现.
析构函数
1.析构函数跟构造函数是对立的,当对象生命周期结束时(例如:对象所在的函数结束调用了)它就会自动
调用析构函数来释放掉自己的空间,避免内存泄漏
2.不管我们有没有编写析构函数,编译器总会为我们写一个默认的析构函数,在调用时先执行自定义的析构
函数,再执行系统默认的析构函数
C++中拷贝构造函数的形参能否进行值传递?
拷贝构造函数:
Cperson(Cperson & copyperson)
作用是用一个已经初始化的类对象做参数去构造一个新对象
它的参数是一个引用,那这个形参能否改成值传递的形式呢?
答案是不能,如果采用值传递的方式,那么在构造的过程中会再次调用拷贝构造函数,进入死循环,导致
无法完成拷贝,栈空间被装满
C++类中能否定义引用类型的数据成员
可以
1.但是要注意引用类型的数据成员必须要在构造参数的初始化列表中进行初始化
2.引用成员不能被重新绑定
在一个成员函数后面加上const是什么意思
表示这个成员函数不会对这个类的成员变量做出修改
当我们在定义成员函数的时候,如果这个函数是不会对成员变量做出修改操作的话,我们就可以在该
函数的后面加上const关键字,这样可以避免一些不必要的错误.
继承
继承分为公有型继承(public)和私有型继承(private)
1.公有型继承是派生类把基类中的公有型成员和保护型成员继承过来,并维持它们原有的访问权限
2.私有型继承是派生类把基类中的公有型成员和保护型成员继承过来,并将他们的访问权限改成私有型
struct关键字和class关键字的区别
struct和class关键字都是用来自定义数据类型的,但是他们的区别主要体现在默认的成员模仿权限和继承
的默认成员访问权限
1.struct的默认成员访问权限是public,而class它的默认成员访问权限是private
2.struct的默认继承访问权限是public,而class的默认继承访问权限是private
class关键字和typename关键字的区别
class和typename都可以用于在C++的模板编程之中声明一种数据类型,但是它们还是有一些对方没有的
功能
1.class关键字可以用于定义一个类,而typename不行
2.typename常用于模板编程中的嵌套从属关系,嵌套依赖关系
如果有一个类中,它对int这个类型起了一个别名,然后我们再接下来的模板编程中,又想用这个新别名去
定义新的变量,那么我们就需要在 T::新别名 前加一个typename告诉编译器,T::新别名 是一个数据类型
而不是一个类的成员变量或者类的成员函数.
隐式类型转换
隐式类型转换主要分两种:算术类型的隐式转换和类类型的隐式转换
1.算术类型的隐式转换主要目的是为了减小精度的丢失
-高精度数和低精度数组合运算时,低精度数会自动隐式转换成高精度数
-无符号数和有符号数运算时,有符号数会被转化成无符号数
-布尔类型和非布尔类型运算时,非布尔类型会被转化成布尔类型
例子:
#include<iostream>
using namespace std;
int main()
{
int a[]={1,2,2,3,4};
int n=sizeof(a);
if(-1>(n/sizeof(int)))
cout<<"-1 is bigger than 5";
else
cout<<"-1 is less than 5";
return 0;
}
2.类类型的隐式类型转换
当构造函数之后一个形参时,我们就可以直接用这一个形参去构造对象
class student{
int num;
student(int num){
this->num=num;
}
}
int main()
{
student stu=1;//隐式类型转换,将int类型转换成student类型
}
如果我们想避免类类型的隐式类型转换,我们可以在构造函数前加explicit关键字
多态
多态主要体现在虚函数上,它通过虚函数的重写,可以让许多功能不同的函数共用一个函数名,
通过虚函数可以实现发送同一个消息给不同的对象时,会产生完全不同的行为(消息主要指函数的调用
不同的行为指的是不同的实现)只要有虚函数存在,对象类型就在程序运行时动态绑定.
例子:现在有一个基类和若干个派生类,基类中有一个虚函数,派生类中又分别对基类中的虚函数做了
重写操作,当你用一个基类的指针分别指向派生类对象后,然后通过基类指针调用虚函数,那么虚函数
具体执行哪一个函数体是取决于基类指针具体指向那个派生类对象的.
ps:详细内容参考后续的RTTI以及虚函数表
纯虚函数与抽象类
1.纯虚函数是指被标明为不具体实现的虚函数,它不具备函数的功能,仅仅起到一个接口的作用
2.抽象类是指包含有纯虚函数的类,一个抽象类至少包含一个纯虚函数;抽象类通常作为父类,从抽象类
中派生出的子类如果不是抽象类,必须要实现父类中的全部纯虚函数.
重载,重写,覆盖 , 隐藏
1.重载:是指两个函数的函数名一样,作用域一样,但是参数列表不一样
2.重写/覆盖:重写和覆盖是一个概念,是指一个虚函数,它在父类中被定义,又在派生类中重新定义了一次
3.隐藏:是指两个函数的函数名一样,但是一个在基类中,一个在派生类中,此时会发生隐藏而非重载,当
使用一个派生类对象调用这个同名函数的时候,其实调用的是子派生类中的函数,因为基类中的函数被隐藏
了,并且基类中所有的同名重载函数都会被隐藏.
为什么析构函数是虚函数
如果说一个类它有可能被继承的话,我们就可以它的析构函数设置为虚函数,这样就可以保证我们在
new一个子类之后,用一个基类的指针去指向这个子类之后,就可以在之后释放基类指针的时候连同把
子类的空间也一同释放了,避免内存泄漏
但是由于虚函数是有自己的虚函数表以及虚表指针的,这些东西都会占用额外的内存,所以如果一个类
不会被继承的话,那么它的析构函数就不必是虚函数.
静态函数和虚函数的区别
1.静态函数在编译器就已经能确定这个函数确切的运行时机,而虚函数是在运行期间采用动态编译,动态
绑定
2.虚函数有自己的虚函数表以及虚表指针,这些都会额外占用一定内存
什么是虚函数表,虚函数表是怎么实现多态的
虚函数表是实现多态的一个重要的机制:
比如说现在有一个基类,和一个派生类,基类中有一个虚函数,派生类中又对这个虚函数做了一个重写
操作,那么我们就可以使用一个基类的指针去指向一个派生类对象,再使用基类指针去调用这个虚函数
那么我们实际调用的其实是派生类中重写的函数,这个机制就是通过虚函数表实现的
1.每个有虚函数的类都会有一个虚函数表,这个表是编译器自动创建的,里面存放的是虚函数的地址
如果这个类被继承的话,那么子类就会继承父类的虚函数表,如果在子类中重载或者添加了虚函数表,那么
子类的虚函数表也会随之更新
2.当调用虚函数时,编译器会通过对象的指针类型或者引用类型去查找对应的虚函数表,然后通过虚函数表
中存放的地址来决定具体调用哪一个函数,以此来实现多态
虚函数的一些限制
1.只有类的成员函数才能成为虚函数
2.静态函数不能是虚函数
3.内联函数不能是虚函数
4.构造函数不能是虚函数,析构函数通常是虚函数
RTTI
RTTI的全称是 运行时类型识别(Run-time Type Identification ,RTTI)
它的作用就是能让程序在运行时通过某一个对象的指针就判断出这个对象确切的数据类型,在实际编程中
往往只提供了某个对象的指针,那么我们想对这个对象操作时,是需要确定这个对象它确切的数据类型的
我们可以利用RTTI快速的通过指针来获取类型并加以控制
在C++中有几个支持RTTI的元素,分别是dynamic_cast强制转换,typeid运算符,type_info类
1.dynamic_cast:之前有讲到过,这是一种动态类型的强制转换,它可以在程序运行时进行类型识别
不管向上转型还是向下转型都是安全的,并且也是一个RTTI的过程.
2.typeid运算符,他可以支持两种参数,类名或者返回值为对象的表达式,它可以在仅仅只提供一个
指针的情况下就可以判断出对象准确的数据类型
3.type_info,typeid运算符的返回值是type_info对象的引用
总结:有了RTTI,我们可以让类设计的更抽象,更符合程序员的逻辑
例子:
class CB
{
virtual void dowork(){};
};
class CF:public CB
{
public:
char * Print(){ return“HELLO”;};
};
int main()
{
CB *p= new CF();
cout<<typeid(*p).name()<<endl;
CF *cf =dynamic_cast<CF*>(P);
if(cf)
cout<<cf->Print()<<endl;
}
ps:这里写的不好理解,更多内容可以其他地方查查资料学一下
C++ STL
set和map的区别与细节
首先map和set两个容器都是通过红黑树来实现的
1.map里面存放的是键值对,key-value,而set是简单的集合,每一个元素只有一个关键字
2.set的迭代器默认是const,不允许修改键值,因为set和map都是通过红黑树实现的,他们都需要根据
关键字来进行排序,所以如果要修改key值的话,就需要先把这个值在红黑树上删除-调整平衡-加入新值-
调整平衡,这样会破坏红黑树的结构使迭代器失效;map也是不允许修改key值的,但是允许修改value的
值
3.map支持下标操作,set不支持,但是如果map的下标是一个不存在的关键字的话,此时就会自动像map
容器中插入一个这个关键字带有一个默认值的元素,所以下标操作可能是不安全的.
set和unordered_set和multiset的区别
map和unordered_map和multimap的区别
1.set和map都是通过红黑树实现的,它们会根据关键字的值进行排序,他们的插入,删除,修改都是
log级别的,而unordered_ser和unordered_map是通过哈希表实现的,它们不需要根据关键字进行排序
所以它们的的插入,修改,删除效率更高,时间复杂度只有常数级别.
2.不管是set还是map,在容器中关键字都是唯一的,但是在multiset中,关键字可以出现多次,在
multimap中,一个关键字可以对应多个值,虽然这四个容器都是通过红黑树实现的,但是multiset
和multimap中关键字允许出现多次.
vector和list的区别
首先vector和list都是在堆上分配内存的
1.vector是连续内存,而list是不连续内存,所以vector可以随机访问,而list是不可以随机访问的
如果想访问一个元素,必须从头节点或者尾结点不断遍历
2.vector有自己独有的扩容机制,当向vector容器中插入一个新元素时,如果此时容器容量没达到上限
那么直接将元素插入到对应位置即可,但是如果此时容器容量满了,vector会申请一个新的空间,新
空间的大小为原来空间元素的2倍,并把原空间的元素按照复制的方式复制到新空间之中,然后对原空间
进行析构和释放;但是list则不同,list是每加入一个新元素时进行一次内存分配,每删除一个元素时进行
一次内存释放
3.vector容器在中间插入元素的时候,如果空间够,会进行内存拷贝,如果空间不够,会进行内存的申请
和释放,再对之前的数据做一个拷贝,但是list是没有内存拷贝的过程的.
size函数和capacity函数的区别
size()函数返回的是当前容器中元素的个数
capacity()返回的是当前容器在重新分配内存之前,最多可以容纳的元素上限
vector容器的扩容机制
具体扩容机制在上面已经讲过了:
在插入新元素时,如果当前容器容量已经满了,vector会申请一个新空间,新空间的大小为原来空间元素
的二倍,然后它会把原空间的元素复制进新空间中,并析构和释放原空间;capacity()函数的返回值也会
变成原来的二倍.
erase函数
使用erase函数删除元素时有可能导致迭代器失效
1.当删除序列容器的元素时(vector,deque)
被删除元素后面的迭代器都会失效,这是因为容器大小发生了改变,元素在内存中的位置改变了
2.删除关联容器元素时
map,set这种红黑树实现的容器时,不会导致除了被删除元素之外的其他元素的迭代器失效,这是因为
红黑树的平衡性保护了迭代器的有效性,包括一些哈希表实现的容器,也不会失效
3.list容器:对于list来说,它使用了不连续分配的内存,并且它的 erase 方法也会返回下一个
有效的 iterator.所以说是不会导致迭代器失效的
resize函数和reverse函数
resize()函数是用来改变容器的元素个数的,它有两种重载形式
1.resize(int count)
如果此时容器元素个数小于count个,那么就在尾部添加若干个默认值,让元素个数达到count个
如果此时容器元素个数大于count个,那么就删除尾部多余的值,让元素个数达到count个
2.resize(int count,int value)
如果此时容器元素个数小于count个,那么就会在尾部添加若干个值为value的值,让元素个数达到count个
如果此时容器元素个数大于count个,那么就删除尾部多余的值,让元素个数达到count个
reverse函数就没什么好说的了,它会接受两个迭代器作为参数,表示区间反转的范围
然后它会把区间中第一个元素和区间中最后一个元素交换,第二个元素和倒数第二个元素交换...以此类推
最后实现区间反转
迭代器和指针的区别
迭代器不是指针,而是类模板,它内部封装了指针和一些其他的信息,并重载了指针的一些运算符
-> * ++ -- 等,从而表现得像指针,比如对于容器list来说,迭代器封装了一个前置指针和一个
后置指针.所以说迭代器是一种比指针更高级的概念,它能提供比指针更高级的行为;迭代器返回
的是对象的一个引用,如果想用迭代器输出元素的话,需要加上*
STL由什么组成
容器 迭代器 仿函数 算法 分配器 配接器
STL分配器
std::allocator是STL的默认分配器,它本质上是一个模板类,功能是动态分配内存和释放内存,主要用于
容器的内存管理
1.动态分配内存,通过成员函数allocate动态分配内存,返回指向该内存的指针
2.释放内存,通过成员函数dealocate释放,它可以接受一个指向先前分配好的内存的指针,并释放它
3.对象的构造和析构,通过成员函数construct和destroy可以在已经分配好的内存上进行对象的构造
和析构
总结:allocator实现了将内存的分配和对象的构造析构分离开来,让容器在实现时专注于自己的数据结构
而不用过多关注于内存管理这些细节.
C++ 内存管理
C语言栈空间的值
windows操作系统下,栈大小为2MB
linux操作系统下,栈大小为8MB
C++虚拟内存都分成哪几个部分
在32位操作系统中,一共有4G可寻址的线性空间,其中
3G是用户空间
1G是内核空间
-------------------------------------------------------------------------------------
C++的虚拟内存主要分为6个部分:代码段,数据段,BSS段,堆区,栈区,文件映射区
其中代码段,数据段,BSS段在静态区域,堆区,栈区,文件映射区在动态区域
代码段:也叫只读代码段,用于存放程序的执行代码,通常情况下,该区域是只读的,确保程序的安全性
和一致性
数据段:包括初始化的全局变量和静态变量,数据段可以进一步分成两个部分
-静态存储区:用于存放全局变量和静态局部变量,在程序的整个生命周期内都存在
-常量存储区:存放常量数据,如字符串常量,该区域只读,不允许修改
BSS段:用于存放未经初始化的全局变量和局部变量,在程序执行前,操作系统会将BSS段中的变量初始化
成0或者其他默认值,所以BSS段不需要存放数据的实际内容,它只需要记录变量大小和出现的位置即可
堆区:用于动态分配内存,用malloc函数或者new关键字来分配内存,与栈区不同的是,堆区需要程序员
显示的操作,而栈区是编译器自动管理
栈区:用于存放函数调用的局部变量和函数调用时的上下文信息,每当进入一个函数时,该函数的局部变量
都会在栈上分配空间,函数调用结束时自动释放.跟堆区不同的是,栈区是编译器自动管理,快速且自动
无需手动干预
文件映射区:将文件的内容映射到虚拟内容中,这样就可以通过访问内存的方式对文件进行读写操作,允许
像访问内存一样访问文件,提供了高效的文件操作方式.
如何定义常量,常量放在内存中那个位置?
在常量名前加const
常量是放入内存中数据段中的常量存储区中的
有人可能会误以为一个全局常量是被存放入静态存储区中,这完全不符合逻辑
静态存储区中的静态变量的值是可以修改的
而常量存储区中的常量的只读的,无法修改
如果一个数据既用static修饰,也用const修饰 ,那么它也是放入常量存储区中的
对以下代码进行分析
const char * arr = "123";
char * brr = "123";
const char crr[] = "123";
char drr[] = "123"
malloc函数的原理
malloc是用来动态分配内存的,它为了减小内存碎片和系统开销,采用的是内存池的方式,首先它会申请
大块内存作为堆区,然后采用隐式链表的方式,将堆区分为若干个连续的,大小不一的,分配或未分配的
内存块,每一个内存块就是内存管理的基本单位. 并且采用显示链表的方式将这些内存块串联起来,就是
用双向链表将它们连接起来,每一个内存块都记录一个连续的,未分配的地址
当进行内存分配时,malloc会遍历隐式链表,找到一个最合适的空闲块进行分配;在进行内存合并时,采
用边界标记法,判断内存块的前一个空闲块和后一个空闲块是否被分配来决定是否要进行合并
当进行内存申请时,如果申请内存小于128k,使用brk系统调用在堆上申请,如果申请内存大于128k,使用
mmap系统调用在映射区上申请.
STL的内存优化
STL采用二级空间配置器
一级空间配置器:
使用malloc,free,realloc等函数进行内存的分配,释放,重排,在内存需求不被满足的时候,调用一个
指定的函数;一级空间配置器只分配128字节以上的空间,如果分配不成功,会调用句柄先释放一部分空间
如果还是不成功则会抛出异常
二级空间配置器:
比一级多了一些限制,会减少内存碎片的产生,避免内存碎片对内存分配产生负担
分配原则:
如果字节大于128字节,交给一级空间配置器
如果小于128字节,则以内存池管理,首先将要分配的字节数提高到8的倍数,然后去自由链表中查看是否
有合适的空闲块,如果有,则返回,如果没有,则向内存池申请数据块(20个),如果内存池不够,那就
有多少给多少,如果一块也给不了,就去堆上申请,堆上如果没有,则会检查自由链表中有没有大的内存块
将其返还给内存池,如果没有,则只能交给一级配置器处理.
空间配置函数allocate:
使用时首先会检查所操作的内存是否大于128字节,若大于,交给一级配置器,若小于,则检查自由链表
返回一个最合适的内存块,如果没有合适的,调用refill函数重新填充空间
空间释放函数deallocate:
使用时首先会检查所操作的内存是否大于128字节,若大于,交给一级配置器,若小于,根据数据块的大小
判断数据块在释放之后插入到那个空闲链表中合适
重新填充函数refill:
当allocate函数查找自由链表找不到合适的空闲块时就会调用refill函数重新填充空间
那么refill函数会像内存池申请数据块(20个),如果内存池不够,有多少给多少,如果内存池一块也
没有,则会去堆上申请,如果堆上也没有,则会查找空闲链表中是否有大内存块将其返还给内存池.
C++11新特性
右值,右值与左值的区别
右值表达的是一个临时的,无名的,即将要被销毁的值,因为它常常出现在赋值操作的右侧,所以也被称为
右值,比如说函数的返回值,局部变量等都可以是右值
右值和左值的区别:
1.左值可以对表达式取引用,而右值不可以
2.左值可以进行赋值操作和修改,而右值不可以
这些区别是因为右值的生命周期太短了,虽然右值也被存放在内存之中,但是因为它的生命周期极短,可能
在表达式结束的时候,它就在内存中被销毁了,所以对右值取引用,赋值,和修改这些操作是没意义的
右值引用
右值引用是C++11新加入的特性,是一个跟移动语义相关的概念,右值引用主要有如下作用:
1.支持移动语义的容器,如vector,string等,这些容器在实现的时候都使用了右值引用+移动语义使得
在插入,删除,重排时能高效进行资源管理
2.资源转移:右值引用可以把一个临时对象或者即将要被删除的对象的资源所有权转移到另一个对象上
转移方式不是拷贝,而是移动,避免了深拷贝的过程,提高性能减小内存开销
3.完美转发:右值引用可以实现泛型代码中的完美转发,通过右值引用和forward函数可以保持传入
函数的参数的值类别是左值还是右值,从而实现参数的完美传递,避免不必要的拷贝和构造
4.提高性能:可以高效的管理临时对象,拒绝不必要的拷贝和构造,灵活的转移资源所有权
移动语义
移动语义也是C++11新引入的语义
移动语义主要通过右值引用和移动构造函数来实现,右值引用的特点就是绑定到临时对象或者即将要被销毁
的对象,移动构造函数的特点就是可以接受一个右值引用作为参数,这样就可以合法有效的转移资源的所有
权,而不需要经过深拷贝的过程
一个最常见的使用场景,在使用vector容器时,如果我们想添加一个新元素进入容器中,我们就可以使用
移动构造函数将资源转移到容器中,而不是通过拷贝操作,这样可以减少内存分配和释放的次数,提高
效率
std::move
std::move的作用是将一个左值引用转换成对应类型的右值引用(将对象的所有权转移而不通过深拷贝)
这意味着后续的操作就可以通过移动构造函数或者移动赋值运算符来操作对象,而不是常规的赋值构造或
拷贝构造
示例:
#include <iostream>
#include <utility>
class MyClass {
public:
MyClass() {
std::cout << "Default constructor" << std::endl;
data = new int[100];
}
MyClass(const MyClass& other) {
std::cout << "Copy constructor" << std::endl;
data = new int[100];
// 深拷贝数据
}
MyClass(MyClass&& other) noexcept {
std::cout << "Move constructor" << std::endl;
data = other.data;
other.data = nullptr;
}
~MyClass() {
delete[] data;
}
private:
int* data;
};
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2(std::move(obj1)); // 使用 std::move() 进行对象转移
return 0;
}
上面类中有一个拷贝构造函数和一个移动构造函数,传统的拷贝构造函数构造的时候
是要把这个对象的资源全部拷贝过来,但是如果资源较大,就会占用性能,而下面的移动
构造函数是通过资源所有权转移的方式来构造对象的,它接收一个右值引用,然后将
右值的所有权转移到我们要构造的对象上.所以我们可以先把左值通过move转化成一个
右值,然后使用移动构造函数,这样可以提高性能.
智能指针
智能指针分为unique_ptr,shared_ptr,weak_ptr
使用智能指针的目的:
当我们动态申请内存之后,如果后面没有手动的去释放的话,就会造成内存泄漏,但是智能指针可以很好
避免这个问题.智能指针实际上是一个类,当它的作用域结束时,会自动调用析构函数来释放掉自己的空间
.所以说当它所在的函数结束时,就代表其作用域也结束了,会自动调用析构函数,不需要手动释放.
unique_ptr:
unique_ptr实现的是一种独占式拥有或者严格性拥有的概念,同一时间只允许有一个智能指针指向该对象
它对于避免内存泄漏是很有效的
shared_ptr:
1、shared_ptr实现的是共享式拥有的概念,它可以允许多个智能指针指向一个对象,并且采用计数机制来
统计当前有多少个智能指针指向该对象,并且可以通过成员函数use_count来查看该值
2.不仅可以使用new来构造,还可以通过引入其他智能指针来构造
3. 当使用 release函数去释放时,只会让其计数器-1,当计数器为0时,才会真正释放资源.
weak_ptr:
weak_ptr是一种弱引用的智能指针,而shared_ptr和unique_ptr是强引用的智能指针
弱引用是指:只有对象的访问手段,但是不能控制对象的生命周期
强引用是指: 既有对象的访问手段,又可以控制对象的生命周期
weak_ptr是为了配合shared_ptr才出现的,它指向的是shared_ptr管理的对象,仅仅提供了对管理对象的
一个访问手段,他可以从一个shared_ptr构造也可以从另一个weak_ptr构造,并且不会引起计数器的增加
或减少.
weak_ptr主要是为了解决两个shared_ptr指针相互引用时产生的死锁问题:
当两个shared_ptr指针相互引用的时候,就会导致循环引用,造成shared_ptr的计数器失效,那么此时计
数器的值就不会下降到0,资源得不到释放,最终会产生内存泄漏.
如上图:parent类中有一个shared_ptr指针指向孩子,而child类中又有一个shared_ptr指针指向父亲
在主函数中,又分别用彼此来构造自己,所以就造成了循环引用.
简单说一下C++11新特性
1.auto关键字:可以根据数据的初始值自动判断出数据类型
2.nullptr:是一种特殊的字面值常量,可以转换成任何其他类型的指针
3.构造参数列表
4.智能指针
5.新加入的容器(array,forward:list,unordered_map,unordered_set)
6.noexcept关键字
还有很多,不一一列举了
noexcept关键字
放在函数声明或者定义的末尾,表示这个函数是不会抛出异常的
class MyClass {
public:
void foo() noexcept {
// 函数体,不会抛出异常
}
void bar() noexcept(false) {
// 函数体,可能抛出异常
}
};
int main() {
MyClass obj;
obj.foo(); // 不会抛出异常
// obj.bar(); // 可能抛出异常,如果抛出异常,程序会终止
return 0;
}
1.因为可以承诺函数是不会抛出异常的,所以可以让调用者更好的设计处理异常情况
2.优化编译器生成的代码,减少异常处理相关的代码和机制,提高效率
但是noexcpet并不是完全安全的,如果在一个noexcept声明的函数中调用了可能产生异常的函数,
那么就违反了noexcept的原则,我们可以在捕获异常之后,手动调用std::terminate函数来终止程序
#include <iostream>
#include <stdexcept>
using namespace std;
void func1() {
throw std::runtime_error("Exception in func1");
}
void func2() noexcept {
try {
func1();
} catch (const std::exception& e) {
std::cout << "Caught exception: " << e.what() << "\n";
std::terminate();
}
}
int main() {
func2();
return 0;
}
C++11新特性(重量级)
1.右值引用(前面有讲过)
2.可变参数模板:
可变参数模板是对参数进行了高度泛化,让它可以表示任意数量,任意类型的函数参数,语法是在class
关键字或者typename关键字后面加上...占位符,占位符有如下作用:
-声明一个包含0到任意数量的函数参数的参数包
-可以将参数包一个一个展开成独立的参数
3.lamda 表达式:
lamda表达式的作用是定义一个匿名函数,用来捕获一定范围内的变量,它跟普通函数的最大区别就是
它可以通过捕获列表来捕获一些上下文中的数据
4.并发编程/多线程编程(放在下面讲)
lamda表达式:
并发编程/多线程编程
参考链接:并发编程
<atomic>:该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套
C 风格的原子类型和与 C 兼容的原子操作的函数。
<thread>:该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
<mutex>:该头文件主要声明了与互斥量(mutex)相关的类,包括 std::mutex 系列类,
std::lock_guard, std::unique_lock, 以及其他的类型和函数。
<condition_variable>:该头文件主要声明了与条件变量相关的类,包括
std::condition_variable 和 std::condition_variable_any。
<future>:该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,
以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,
std::async() 函数就声明在此头文件中。
一个简单使用thread的例子:
#include <stdio.h>
#include <stdlib.h>
#include <iostream> // std::cout
#include <thread> // std::thread
void thread_task() {
std::cout << "hello thread" << std::endl;
}
/*
* === FUNCTION =========================================================
* Name: main
* Description: program entry routine.
* ========================================================================
*/
int main(int argc, const char *argv[])
{
std::thread t(thread_task);
t.join();
return EXIT_SUCCESS;
} /* ---------- end of function main ---------- */
C++ linux编程+I/O多路复用
fork函数
1.用于创建一个跟父进程几乎一模一样的子进程,几乎赋值了父进程的所有内容(代码,数据,文件描述符)
在父进程中,如果fork函数创建子进程成功,那么返回的则是子进程的pid,如果创建失败,返回的则是一个
负数,在子进程中,fork函数返回的是0
2.fork函数提供了一种新的进程创建方式,通常用于并发编程或多进程应用程序
wait函数
1.wait函数用于使父进程等待子进程的结束,当使用wait函数时,父进程会被阻塞,直到子进程结束,如果
子进程结束,wait函数会立刻返回,结束父进程的阻塞状态.
2.wait函数还可以用于获取子进程的终止状态,了解子进程的执行结果.
exec函数
exec函数用于在一个进程中创建一个新的程序
1.他可以把原有进程的地址空间替换为新的地址空间,然后开始执行新程序的代码
2.它一般跟fork函数联合使用,先用fork函数创建一个子进程,然后用exec函数替换掉原有进程的二进制
映像,这样就可在子进程中执行新程序
一个C语言从文本到可执行文件的过程
一共要经历4个阶段,分别是预处理阶段,编译阶段,汇编阶段,链接阶段
1.预处理阶段,在这个阶段进行头文件展开,宏定义替换,条件编译,删除注释等
将.c文件或.cpp文件变成 .i文件
2.编译阶段,将预处理文件编译成汇编文件
.i文件变成.s文件
3.汇编阶段,将汇编文件转换成机器码,变成可以重定位的目标文件
.s文件变.o文件
4.把目标文件和它们所需要的库链接起来,变成可执行文件
ps:.c文件和.cpp文件不能混合编译,要分别先变成目标文件,再链接成可执行文件
单线程如何处理高并发
对于单线程模型,可以采用I/O多路复用技术,提高单个线程处理多个请求的能力,然后使用事件驱动
模型,基于异步回调的方式处理事件.
I/O多路复用技术
I/O多路复用
更多内容在即将发布的博客中
未完待续!!