C、C++ 基础
C++文件编译与执行的四个阶段
- 第一阶段:预处理阶段。根据文件中的预处理指令来修改源文件的内容。如#include指令,作用是把头文件的内容添加到.cpp文件中。
- 第二阶段:编译阶段,将其翻译成等价的中间代码或汇编代码。
- 第三阶段:汇编阶段,把汇编语言翻译成目标机器指令。
- 第四阶段:是链接,例如,某个源文件中的函数可能引用了另一个源文件中定义的某个函数;在程序中可能调用了某个库文件中的函数。
定义与声明区别
声明是告诉编译器变量的类型和名字(extern关键字可以进行声明),不为变量分配空间,而定义变量需要分配空间,同一个变量只能被定义一次,但可以被声明多次。
c语言和c++有什么区别?
- C++ 在 c 的基础上添加类
- C主要是面向过程,C + + 主要面向对象
- C主要考虑通过一个过程将输入量经过各种运算后得到一个输出, C++ 主要考虑是如何构造一个对象模型,让这个模型契合与之对应的问题域, 这样就可以通过获取对象的状态信息得到输出。
封装、继承、多态
封装::将数据以及基于数据的一系列操作封装在一个对象中,各对象之间相互独立;隐藏对象的属性和实现细节,对外提供访问接口。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
继承:使子类具有父类的属性和方法,实现代码重用,节省开发时间。(子类也可定义自己的属性和方法,并根据自己需求重写父类方法,使自己获得与父类不同的功能)。
多态:由继承而产生的相关的类,其对象对同一消息会做出不同的响应。即同一操作作用于不同对象时产生不同的执行结果。
面向过程与面向对象的区别
面向过程程序设计是围绕功能进行的,用一个函数实现一个功能。所有的数据都是公用的,一个函数可以使用任何一组数据,而一组数据又能被多个函数使用。程序设计者必须考虑每个细节,是什么时候对什么数据进行操作,这样当程序规模较大的时候就会觉得难以应付。
面向对象是把数据和与其相关的操作放在一起,封装成了一个对象,与外界相对分隔,这样就不必考虑过多无关细节,程序设计者只需要考虑把哪些数据和操作封装在一起以及怎样向有关对象发送消息完成所需的任务就行了,大大降低了工作难度并提高了效率。
智能指针怎么实现?什么时候改变引用计数?
- 构造函数中计数初始化为1。
- 拷贝构造函数中计数值加1。
- 赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一。
- 析构函数中引用计数减一。
- 在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。
四种强制类型转换
- static_cast( expression )
用于数值类型之间的转换,也可以用于指针之间的转换,编译时已经确定好,效率高,但需要保证其安全性。 (1) 指针要先转换成void才能继续往下转换。 (2) 在基类和派生类之间进行转换(必须有继承关系的两个类)
子类对象可以转为基类对象(安全),基类对象不能转为子类对象(可以转换,但不安全dynamic_cast可以实现安全的向下转换)。static_cast不能转换掉expression的const、volatile、或者__unaligned属性。 - dynamic_cast < type-id> ( expression )
只能用于对象的指针和引用之间的转换,需要虚函数。
dynamic_cast会检查转换是否会返回一个被请求的有效的完整对象,否则返回NULL;
Type-id必须是类的指针、类的引用或者void *
,用于将基类的指针或引用安全地转换成派生类的指针或引用。 - const_cast < type-id> ( expression )
这个转换类型操纵传递对象的const属性,或者是设置或者是移除。 - reinterpret_cast < type-id> ( expression )
用在任意指针类型之间的转换;以及指针与足够大的整数类型之间的转换,从整数到指针,无视大小。
C++是不是类型安全的?
不是。两个不同类型的指针之间可以强制转换。
0 的比较判断
//int型变量
if ( n == 0 )
if ( n != 0 )
//bool型
if (value == 0)
if (value != 0)
//char*型
if(p == NULL) / if(p != NULL)
//浮点型
const float EPSINON = 0.0000001;
if ((x >= - EPSINON) && (x <= EPSINON)
C++11有哪些新特性
- 关键字及新语法:auto、nullptr、for
- STL容器:std::array、std::forward_list、std::unordered_map、std::unordered_set
- 多线程:std::thread、std::atomic、std::condition_variable
- 智能指针内存管理:std::shared_ptr、std::weak_ptr
- 其他:std::function、std::bind和lamda表达式
结构体和联合体的区别
结构体和联合体都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合体中只存放了一个被选中的成员,而结构体的所有成员都存在。
联合体类型的变量,所有成员变量共享一块内存,该内存的大小有这些成员变量中长度最大的一个来决定,而结构体中成员变量内存都是独立的。因此,对于union的一个成员赋值, 那么其它成员会重写,而struct则不会。
联合体分配的内存是连续的,而结构体不能保证分配的内存是连续的。
C++中类与结构体的区别
class
继承默认是private
继承,而struct
默认public
继承。- 在默认情况下,
struct
的成员变量是公共public
的;在默认情况下,class
的成员变量是私有private
的。 class
还用于定义模板参数,就像typename
,而关键字struct
不能。
宏定义
宏定义
C/C++语言中,预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换,预处理过程还会删除程序中的注释和多余的空白符号。预处理指令是以#
开头的代码行,#
必须是该行除了空白字符外的第一个字符。#
后是指令关键字,在#
和指令关键字之间允许存在若干空白字符。
#define
命令是C语言中的一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。
该命令有两种格式:一种是简单的宏定义,另一种是带参数的宏定义。
- 简单的宏定义:
#define 宏名 字符串
- 带参数的宏定义
#define 宏名(参数表) 宏体 //此时宏名与参数表之间不能有空格
在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,这种替换没有任何计算功能。
宏定义的优缺点
- 提高了程序的可读性,方便进行修改;
- 提高程序的运行效率:使用带参的宏定义既可完成函数调用的功能,又能避免函数的出栈与入栈操作,减少系统开销,提高运行效率;
- 宏是由预处理器处理的,通过字符串操作可以完成很多编译器无法实现的功能。比如
##
连接符。 - 由于是直接嵌入的,所以代码可能相对多一点;
- 嵌套定义过多可能会影响程序的可读性,而且很容易出错;
- 对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。
条件编译
使用条件编译,方便程序员在调试程序的过程中,执行一些在程序发布后并不需要执行的指令。只要在需要调试的代码前加上_DEBUG
的定义,就可以在调试程序的过程中输出调试信息。这样方便我们查看程序在运行过程中有没有出现错误,定位错误出现的地方。而在程序发布之前,取消_DEBUG
的定义就可以不再执行调试代码。
#ifdef _DEBUG
//如果定义了_DEBUG,则执行#ifdef _DEBUG与#else之间的指令;
cout<<"debug"<<endl;
#else
//否则,执行#else与#endif之间的指令。
cout<<"release"<<endl;
#endif
避免头文件重复引用的宏定义
“被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。
#ifndef A_H
#define A_H
//a.h文件需要提供的接口声明
#endif
宏函数
函数的调用是需要一定的时间和空间代价的。因为系统在调用函数时,需要保留"现场",即将程序要执行的指令的下一条指令的位置压入栈,然后转入调用函数去执行,调用完函数后再返回主调函数,恢复"现场",返回到栈里保存的的下一条指令的位置继续执行。所以函数的调用需要额外的时间和空间代价。
而宏函数则不存在上述问题,宏函数在预编译时,同函数定义的代码来替换函数名,将函数代码段嵌入到当前程序,不会产生函数调用,所以会省去普通函数保留现场恢复现场的时间,但因为要将定义的函数体嵌入到当前程序,所以不可避免的会占用额外的存储空间。在频繁调用同一个宏的时候,该现象尤其明显。
宏函数的示例定义如下:
#define MAX(a,b) ((a)<(b)?(b):(a))
宏函数的优点在于避免函数调用,提高程序效率。
使用宏函数需要注意边缘效应:
#define N 2+3
int a = N/2;
我们预想的a的值是2,可实际上a的值是3。原因在于在预处理阶段,编译器将 a = N/2
处理成了 a = 2+3/2;
这就是宏定义的字符串替换的“边缘效应”因此要如下定义:
#define N (2+3)
#define特殊用法
x##y
#define Conn(x,y) (x##y)
表示x
连接y
:
int n = Conn(123,456); //结果就是n=123456;
#@x
#define ToChar(x) (#@x)
给x
加上单引号,结果返回是一个const char
char a = ToChar(1);//结果就是a='1';
#x
#define ToString(x) (#x)
给x加双引号
char* str = ToString(123132);//就成了str="123132";
枚举和#define的区别
- 宏定义常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
- 一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
- 枚举可以一次定义大量相关的常量,而宏一次只能定义一个。
const和#define的区别
编译器处理方式
- define : 在预处理阶段进行替换 。
- const :在编译时确定其值。
类型检查
- define :无类型,不进行类型安全检查,可能会产生意想不到的错误 。
- const :有数据类型,编译时会进行类型检查。
内存空间
- define :不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大 。
- const : 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝。
作用范围
宏定义的作用范围仅限于当前文件。
默认状态下,const对象只在文件内有效,当多个文件中出现了同名的const变量时,等同于在不同文件中分别定义了独立的变量。 如果想在多个文件之间共享const对象,必须在变量定义之前添加extern关键字(在声明和定义时都要加)。
typdef和#define的区别
- 原理不同。
#define
是预处理指令,在预处理时进行简单而机械的字符串替换,不做类型检查,只有在编译时才能发现错误;而typedef
是关键字,在编译时处理,具有类型检查功能。 - 功能不同。
typedef
用来定义类型的别名,而#define
不只是可以为类型区别名,还可以定义常量、变量等等。 - 对指针的操作不同。
#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 p1,p2; #定义了一个int型指针p1,和一个int型变量 p2
INTPTR2 p1,p2; # p1,p2都是指针变量
内联函数与#define的区别
- 内联函数在编译时展开,而宏在预编译时展开。
- 在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
- 内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。
- 宏不是函数,而inline是函数
extern 关键字
extern "C"
如:extern "C" void fun(int a,int b);
则告诉编译器在编译fun这个函数时候按着C的规矩去翻译,而不是C++的(这与C++的重载有关,C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同)。C++语言支持函数重载,C 语言不支持函数重载。函数被C++编译后在库中的名字与C 语言的不同。假设某个函数的原型为:void foo(int x, int y);
该函数被C 编译器编译后在库中的名字为_foo
, 而C++ 编译器则会产生像_foo_int_int
之类的名字。extern用在变量或者函数的声明前
用来说明“此变量/函数是在别处定义的,要在此处引用”。extern声明不是定义,即不分配存储空间。也就是说,在一个文件中定义了变量和函数, 在其他文件中要使用它们, 可以有两种方式:使用头文件,然后声明它们,然后其他文件去包含头文件;在其他文件中直接extern。
extern 与实际定义必须一致
在一个源文件里定义了一个数组:char a[6];
在另外一个文件里用下列语句进行了声明:extern char *a;
这样可以吗?
不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型T的数组。extern char *a
声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]
。
volatile关键字
volatile
是一个类型修饰符,被volatile
修饰的变量表示该变量可能会被意想不到的改变,变量被这个关键字声明后,系统对这种变量的处理不会做优化,从而可以提供对特殊地址的稳定访问。(准确的说,定义变量后系统每次用到它的时候都是直接从对应的内存当中提取,而不是使用保存在寄存器里的备份)。volatile
一般用于修饰多线程间被多个任务共享的变量和并行设备硬件寄存器等。
static 关键字
静态局部变量
在局部变量前加上static
关键字,函数即被定义为静态函数。
- 静态局部变量在静态存储区内分配内存单元,直到整个程序运行结束才会释放。
- 静态局部变量的内存只被分配一次,其值在下次调用函数时仍维持上次的值。
- 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0。
- 静态局部变量作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。
静态全局变量
在全局变量前加上static
关键字,函数即被定义为静态函数。
- 静态全局变量静态存储区分配内存单元,未经初始化的全局静态变量会被程序自动初始化为0,内存只被分配一次。
- 静态全局变量只在定义它的文件内有效,而全局变量在整个工程文件内都有效。
静态函数
在函数的返回类型前加上static
关键字,函数即被定义为静态函数。
- 静态函数只能在声明它的文件当中可见,不能被其它文件使用。
- 其它文件中可以定义相同名字的函数,不会发生冲突。
类的静态数据成员
在类内数据成员的声明前加上关键字static
,该数据成员就是类内的静态数据成员。
- 类的静态数据成员被当作是类的成员,并不属于某个对象。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。对于非静态数据成员,每个类对象都有自己的拷贝。
- 类的静态数据成员存储在全局数据区,静态数据成员定义时要分配空间,所以不能在类声明中定义。
- 类的静态数据成员和普通数据成员一样遵从public,protected,private访问规则。
- 类的静态数据成员属于本类的所有对象共享,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它。
- 类的静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:
<数据类型><类名>::<静态数据成员名>=<值>
。 - 类的静态数据成员有两种访问形式:
<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
。 - 不能将静态成员函数定义为虚函数。
同全局变量相比,使用类的静态数据成员有两个优势:
- 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性。
- 可以实现信息隐藏,静态数据成员可以是private成员,而全局变量不能。
静态成员函数
- 出现在类体外的函数定义不能指定关键字
static
。 - 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数。
- 静态成员函数可以任意地访问静态成员函数和静态数据成员。
- 静态成员函数不能访问非静态成员函数和非静态数据成员。
- 静态成员函数由于不是与任何的对象相联系,因此它不具有
this
指针。由于没有this
指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长。 - 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:
<类名>::<静态成员函数名>(<参数表>)
。 - 不可以同时用
const
和static
修饰成员函数,C++编译器在实现const
的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*
。但当一个成员为static
的时候,该函数是没有this
指针的。也就是说此时const的用法和static是冲突的。
const 关键字
定义常量
const修饰变量,以下两种定义形式:
TYPE const ValueName = value;
const TYPE ValueName = value;
它的含义是:const修饰的类型为TYPE的变量value是不可变的。
指针使用const
const修饰指针,涉及到两个很重要的概念,顶层const和底层const
指针自身是一个对象,它的值为一个整数,表明指向对象的内存地址。进而,指针本身是否是常量以及所指向的对象是否是常量就是两个独立的问题。
顶层const(top-level const): 指针本身是个常量;
底层const(low-level const): 指针指向对象是一个常量;
- 指针本身是常量不可变
char* const pContent;
- 指针所指向的内容是常量不可变
const char *pContent;
- 两者都不可变
const char* const pContent;
如果const位于*
的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。
函数中使用const
const 修饰函数参数
- const 修饰函数参数
void function(const int Var);
- 参数指针所指内容为常量不可变
void function(const char* Var);
- 参数指针本身为常量不可变
void function(char* const Var);
- 参数为引用,为了增加效率同时防止修改。修饰引用参数
void function(const TYPE& Var);
const 修饰函数返回值
const int fun1()
这个其实无意义,因为参数返回本身就是赋值。
const int * fun2()
调用时const int *pValue = fun2();
我们可以把fun2()
看作成一个变量,即指针内容不可变。
int* const fun3()
调用时int * const pValue = fun2();
我们可以把fun2()
看作成一个变量,即指针本身不可变。
一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。
const修饰成员变量
const修饰类的成员函数,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。
const修饰成员函数
const修饰类的成员函数,一般写在函数的最后来修饰。
- const成员函数不允许修改它所在对象的任何一个数据成员。
- const成员函数能够访问对象的const成员,而其他成员函数不可以,即普通成员函数不能访问对象内的const数据成员。
const修饰类对象/对象指针/对象引用
- const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。对于对象指针和对象引用也是一样。
- const修饰的对象,该对象的任何非const成员函数都不能被调用,因为任何非const成员函数会有修改成员变量的企图。
class A
{
void func1();
void func2() const;
}
const A aObj;
aObj.func1(); //错误
aObj.func2(); //正确
const A* aObj = new A();
aObj-> func1(); //错误
aObj-> func2(); //正确
将const类型转化为非const类型的方法
const_cast <type_id> (expression)
- 常量指针被转化成非常量指针,并且仍然指向原来的对象;
- 常量引用被转换成非常量引用,并且仍然指向原来的对象;
- 常量对象被转换成非常量对象。
C++ 中const与C语言中const的区别
C++中,const变量默认是内连接的。也就是说它只能在定义它的文件内部使用,链接时其它编译单元看不见它。若要改为外部连接,则需要使用extern声明。
C语言中的const默认为外链接。
const int bufsize = 100;
char buf[bufsize];
在C++中正确,编译期间可以知道bufsize的值,在C语言中不正确,编译期间并不知道bufsize的值。
const int size;
这种定义形式在C语言中是正确的,因为它被C编译器看作一个声明,指明在别的地方分配存储空间。
但在C++中这样写是不正确的,必须写成:
extern const int size
或者:
const int bufsize = 100;
内连接与外连接
编译是以源文件cpp文件为单位,编译成一个个的obj文件,然后再通过链接器把不同的obj文件链接起来。
内外链接的区别就在于:编译期间是否能够交互。
- 内部连接:如果一个名称对于它的编译单元来说是局部的,并且在连接时不会与其它编译单元中的同样的名称相冲突,那么这个名称有内部连接。
- 外部连接:在一个多文件程序中,如果一个名称在连接时可以和其它编译单元交互,那么这个名称就有外部连接。
内存
五个分区
堆 heap
由new分配的内存块,其释放编译器不去管,由我们程序自己控制(一个new对应一个delete)。如果程序员没有释放掉,在程序结束时OS会自动回收。涉及的问题:“缓冲区溢出”、“内存泄露”
栈 stack
是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。
存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。
全局/静态存储区 (.bss段和.data段)
全局和静态变量被分配到同一块内存中。在C语言中,未初始化的放在.bss段中,初始化的放在.data段中;在C++里则不区分了。
常量存储区 (.rodata段)
存放常量,不允许修改(通过非正当手段也可以修改)。
代码区 (.text段)
存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)。
分区原因:
- 一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟 空间以方便访问和节约空间。
- 临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。
- 全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。
C++ 内存分配
根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即自由存储区,动态区、静态区。
- 栈上分配:局部非静态变量的存储区域,即平常所说的栈 。
- 堆上分配: 用operator new ,malloc分配的内存,即平常所说的堆 。
- 静态存储区分配:全局变量 静态变量 字符串常量存在位置。
而代码虽然占内存,但不属于c/c++内存模型的一部分。
堆和栈的区别
申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b
; 系统自动在栈中为b
开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如:p1 = (char *)malloc(10)
;
在C++中用new运算符
如:p2 = (char *)malloc(10)
;
但是注意p1、p2本身是在栈中的。
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,进程栈的大小64bits的Windows默认1MB,64bits的Linux默认10MB;如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
申请效率的比较
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
堆和栈中的存储内容
栈 :在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
内存泄漏
定义
指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
类型
- 堆内存泄漏(Heap leak)。堆内存指的是程序执行中依据须要分配通过malloc,realloc new等从堆中分配的一块内存,在使用完毕后必须通过调用相应的 free或者delete 删掉。假设程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。
- 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比方 Bitmap,handle ,SOCKET等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
防范
使用了内存分配的函数,一旦使用完成,要记得要使用其想用的函数释放掉。
Heap memory:
malloc\realloc ------ free
new \new[] ---------- delete \delete[]
GlobalAlloc------------GlobalFree
Resource Leak :对于系统资源使用之前要细致看起用法,防止错误使用或者忘记释放掉系统资源。
内存溢出
所谓内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是会产生内存溢出的问题。
常见的溢出主要有:
- 内存分配未成功,却使用了它。
经常使用解决的方法是,在使用内存之前检查指针是否为NULL。假设指针p 是函数的參数,那么在函数的入口处用assert(p!=NULL)进行检查。假设是用malloc 或new 来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。 - 内存分配尽管成功,可是尚未初始化就引用它。
- 内存分配成功而且已经初始化,但操作越过了内存的边界,比如数组越界。
new/delete与malloc/free
new/delete与malloc/free区别
new
能够在自动计算需要分配的内存空间,而malloc
需要手工计算字节数。
int*p=new int[2],int*q = malloc(2*sizeof(int))
new
可调用构造函数,malloc
不能;delete
可调用析构函数而free
不能。malloc/free
是C/C++语言的标准库函数,需要库文件stdlib.h支持,delete/free
是C++运算符,不能在C语言中使用new/delete
返回的是具体类型的指针,malloc/free
返回void类型指针。
delete和delete []的区别
都是用来调用析构函数的:
- delete只会调用一次析构函数,delete[]会调用每一个成员的析构函数。
- delete与new配套,delete []与new []配套,用new分配的内存用delete删除用new[]分配的内存用delete[]删除
指针
常量指针和指针常量
常量指针:指向常量的指针,因为常量指针指向的对象是常量,因此这个对象的值不能改变,但可以改变指针的指向,即可以指向不同的常量,但不能改变所指的对象的值。
int const *p;
const int *p;
指针常量:定义的指针只能在定义的时候初始化,之后不能改变指向。
int* const p = &a;
函数指针和指针函数
函数指针:指向函数的指针变量,即本质是一个指针变量,表示的是一个指针,指向的是一个函数。
int (*p)(int x,int y) //(*p)指向一个函数
指针函数:指的是带指针的函数,本质是一个函数,返回类型是指针类型。
int *f(int x,int y)
指针数组、数组指针
指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。它是“储存指针的数组”的简称。
数组指针:首先它是一个指针,它指向一个数组。在32 位系统下任何类型的指针永远是占4 个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小。它是“指向数组的指针”的简称。
int arr[] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&arr + 1); //2 5
cout << *(arr + 1) << " " << *(ptr - 1) << endl;
ptr = (int *)(arr + 1); //2 1
cout << *(arr + 1) << " " << *(ptr - 1) << endl;
- 数组名arr可以作为数组的首地址,而&arr是数组的指针。
- arr和&arr指向的是同一块地址,但他们+1后的效果不同,arr+1是一个元素的内存大小(增加4)而&arr+1增加的是整个数组的内存。
数组指针(行指针):
int a[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
int(*p)[3];
p = a;
p++;
cout << **p << endl;
野指针和空指针
野指针:指向不可用内存的指针。
- 任何指针变量在被创建时不会自动成为NULL指针(空指针),默认值是随机的,所以指针被创建时应被初始化或置为NULL,否者会成为野指针。
- free或delete释放内存,是把指针指向的内存释放了,但并没有把指针本身释放,指针如果未设置为NULL就会不指向合法的内存,成为野指针。
- 指针操作超越了变量的作用范围。例如int a[4],指针指向了a[5]。
空指针:是一个特殊的指针,唯一一个对任何指针类型都合法的指针。指针设置为0或NULL都代表空指针。
指针和引用
引用:就是某个目标变量的“别名”,对应用的操作与变量直接操作效果完全相同。
指针是一个变量,该变量专门存放内存地址。
- 引用必须被初始化,指针不必。
- 引用初始化以后不能被改变,指针可以改变所指的对象。
- 不存在指向空值的引用,但是存在指向空值的指针。
- “sizeof 引用" = 指向变量的大小 , "sizeof 指针"= 指针本身的大小。
- 作为参数传递时,二者有本质不同:指针传参本质是值传递,被调函数的形参作为局部变量在栈中开辟内存以存放由主调函数放进来的实参值,从而形成实参的一个副本。而引用传递时,被调函数对形参的任何操作都会通过一个间接寻址的方式影响主调函数中的实参变量。
数组与指针的区别
sizeof
内存中数据对齐
计算机中内存空间中各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是数据对齐。
- 访问数据的内存地址要满足一定的条件:能被这个数据的长度所整除。 例如,1字节数据已经是对齐的,2字节的数据的地址要被2整除,4字节的数据地址要被4整除。
- 数据对齐并不是操作系统的内存结构的一部分,而是C P U结构的一部分。当C P U访问正确对齐的数据时,它的运行效率最高。
- 字节对齐是在编译时决定的,一旦决定则不会改变,因此即使有对齐的因素,也不会出现一个结构在运行时尺寸发生变化。
sizeof 基本数据类型
sizeof 结构体
结构体的sizeof
是所有成员对齐后长度相加,而union共用体的sizeof
是取最大的成员长度。
结构体按照最大成员长度对齐,占用内存是即最大成员的整数倍。
struct A1 {
int a;
char c;
A1();
~A1(){}
};
struct A2 {
double d;
float f;
int a;
char c;
A2();
~A2() {}
};
int main(int argc, char* argv[]) {
cout << sizeof(A1) << endl; //8
cout << sizeof(A2) << endl; //24
return 0;
}
sizeof 只计算栈中分配的大小
struct A1 {
int a;
char c;
static float c1; //BSS段,不属于栈区
A1();
~A1(){}
};
struct A2 {
int a;
char c;
float c1;
A2();
~A2(){}
};
int main(int argc, char* argv[]) {
cout << sizeof(A1) << endl; //8
cout << sizeof(A2) << endl; //12
return 0;
}
sizeof 指针和引用
指针进行sizeof操作得到的是指针本身(所指向的变量(对象)的地址)的大小。
struct A {
int a;
char c;
};
int main(int argc, char* argv[]) {
A a;
A &b = a;
cout << sizeof(b) << endl; //8
A *c = &a;
cout << sizeof(c) << endl; //4
return 0;
}
数组作为参数传递给函数时传的是指针而非数组,传递的是数组的首地址。
int g(char ch[])
{
return sizeof(ch);
}
int _tmain(int argc, _TCHAR* argv[])
{
char ch[6];
cout << sizeof(ch) << endl; // 6
cout << g(ch) << endl; //4
return 0;
}
sizeof 与 类
sizeof 之虚函数
虚函数是由虚函数表和虚表指针来动态绑定的,在计算sizeof
时,无论有多少个虚函数,其只计算sizeof
(虚表指针)=4(64位为8)。
class A1 {
int a;
char c;
static char c1; //BSS段,不属于栈区
A1();
~A1() {}
virtual void f1() {}
virtual void f2() {}
virtual void f3() {}
};
int main(int argc, char* argv[])
{
cout << sizeof(A1) << endl; //12
return 0;
}
sizeof 之继承
- 基类的sizeof结果只与基类有关。
- 因存在继承关系,所以派生类的sizeof结果需要加上基类的sizeof结果。
- 当基类和派生类均有虚函数时,只计算一次sizeof(虚表指针)。
class A1 {
int a;
char c;
static char c1; //BSS段,不属于栈区
A1();
~A1() {}
virtual void f1() {}
virtual void f2() {}
virtual void f3() {}
};
class A2 : public A1{
float f;
void f1(){}
void f2(){}
void f3(){}
virtual void g1(){}
virtual void g2(){}
};
int main(int argc, char* argv[])
{
cout << sizeof(A1) << endl; //12,class A1占用12Byte
cout << sizeof(A2) << endl; //16,需要加上class A1占用12Byte
getchar();
return 0;
}
sizeof 之空类
class A1 {
};
class A2 : public A1{
};
int main(int argc, char* argv[])
{
cout << sizeof(A1) << endl; //1
cout << sizeof(A2) << endl; //1
return 0;
}
sizeof 之成员函数
一个类中,虚函数本身、成员函数(包括静态与非静态)都是不占用类对象的存储空间的。
class A1 {
void f1();
virtual int f2();
static int f3();
};
int main(int argc, char* argv[])
{
cout << sizeof(A1) << endl; //4
return 0;
}
sizeof 之对象
class A1 {
public:
void f1();
static int f3();
private :
int a;
static int b;
char c;
};
int main(int argc, char* argv[])
{
A1 a1;
cout << sizeof(a1) << endl; //8
return 0;
}
sizeof 之虚继承
虚承继的情况:需要增加一个虚表指针,因而增加4byte
class A {
public:
int a;
};
class B : virtual public A
{
public:
int b;
};
class C : virtual public B {
};
int main()
{
cout << sizeof(A) << endl; //4
cout << sizeof(B) << endl; //12
cout << sizeof(C) << endl; //16
return 0;
}
sizeof 之多重继承
class A {
public:
int a;
};
class B : virtual public A {
};
class C : virtual public A {
};
class D : public B, public C{
};
int main()
{
cout << sizeof(A) << endl; //4
cout << sizeof(B) << endl; //8
cout << sizeof(C) << endl; //8
//这里需要注意要减去4,因为B和C同时继承A,只需要保存一个A的副本就好了,sizeof(D)=4(A的副本)+4(B的虚表)+4(C的虚表)=12
cout << sizeof(D) << endl; //12
return 0;
}
sizeof 虚继承虚函数
class A
{
public:
virtual void aa() { }
virtual void aa2() { }
private:
char ch[3];
};
class B : virtual public A
{
};
class C : virtual public A
{
public:
virtual void bb() { }
virtual void bb2() { }
};
int main(void)
{
cout << sizeof(A) << endl; // 8
cout << sizeof(B) << endl; // 12
cout << sizeof(C) << endl; // 16
return 0;
}
sizeof 和strlen 的区别
- sizeof是关键字,strlen是函数。
cout << strlen("\0") << endl; //0
cout << sizeof('\0') << endl; //1,被当作字符来处理
cout << sizeof("\0") << endl; //2,被当作字符串来处理,默认添加'\0'作为结尾
char str[] = "0123456789";
cout << strlen(str) << endl; //10
cout << sizeof(str) << endl; //11
strlen
执行的计数器的工作,它从内存的某个位置开始扫描,直到碰到第一个字符串结束符’\0’
为止,然后返回计数器值。sizeof
是关键字,它以字节的形式给出了其操作数的存储大小,操作数可以是一个表达式或类型名,操作数的存储大小由操作数的类型决定。
sizeof
类型(自定义类型)、函数作为参数,strlen只
能char*
作为参数,而且必须是以‘\0’结尾。sizeof
以函数作为参数,例如int g()
,则sizeof(g())
的值等价于sizeof(int)
的值。sizeof
后如果是类型需要加括号,如果是变量则不需要加括号,这是因为sizeof
是操作符而不是函数。
int a;
cout << sizeof a << endl; //无需加括号,正确
cout << sizeof (int) << endl; //需要加括号
- 大部分编译程序的
sizeof
都是在编译的时候计算,而strlen
在运行期间计算。
char ch[6];
int num[sizeof(ch)]; //真确
int num1[strlen(ch)]; //错误
字符串
strlen
solution1
int my_strlen(const char* str)
{
assert(str);
int count = 0;
while (*str++)
{
count++;
}
return count;
}
solution 2
int my_strlen(const char* str)
{
assert(str);
if (*str == '\0')
return 0;
else
return 1 + my_strlen(++str);
}
注意,上面不能使用str++
。
solution 3
int my_strlen(char* str)
{
assert(str);
char * ret = str;
while (*ret != '\0')
{
ret++;
}
return ret - str;
}
这里的参数不能使用const
类型,否则无法赋值给char*
。
strcpy
char * my_strcpy( char *strDest, const char *strSrc )
{
assert(strDest && strSrc);
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘\0’ );
return address;
}
strncpy
char * strncpy(char *strDest, const char *strSrc, int num)
{
assert((strDest != NULL) && (strSrc != NULL));
char *strDestcopy = strDest;
while ((num--) && (*strDest++ = *strSrc++) != '\0');
if (num > 0)
{
while (--num)
{
*strDest++ = '\0';
}
}
return strDestcopy;
}
strcat
char* my_strcat(char *dest, char *str)
{
char *ret = dest;
assert(dest && str);
while (*dest)
{
dest++;
}
while (*dest++ = *str++)
{
;
}
return ret;
}
strstr
char *my_strstr(const char *s1, const char *s2)
{
int len2;
if (!(len2 = strlen(s2)))
return (char *)s1;
for (; *s1; ++s1)
{
if (*s1 == *s2 && strncmp(s1, s2, len2) == 0)
return (char *)s1;
}
return NULL;
}
strcmp
int my_atrcmp(const char str1[], const char str2[])
{
assert(str1 && str2);
int ret = 0;
while (!(ret = *(unsigned char *)str1 - *(unsigned char *)str2) && *str1)
{
str1++;
str2++;
}
if (ret < 0)
return -1;
else if (ret > 0)
return 1;
else
return 0;
}
覆盖、重载、隐藏
重载
重载:重载翻译自overload,是指同一可访问区内被声明的几个具有不同参数列表(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
成员函数被重载的特征:
- 相同的范围(在同一个类中);
- 函数名字相同;
- 参数不同;
- virtual 关键字可有可无。
重写
重写:重写(覆盖)翻译自override,是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致,只有函数体不同。
覆盖是指派生类函数覆盖基类函数,特征是:
- 不同的范围(分别位于派生类与基类);
- 函数名字相同;
- 参数相同;
- 基类函数必须有virtual 关键字。
隐藏
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
- 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
类的构造与析构
类的默认定义
构造函数、析构函数、赋值函数是每个类最基本的的函数。每个类只有一个析构函数和一个赋值函数。但是有很多构造函数(一个为复制构造函数,其他为普通构造函数。对于一个类A,如果不编写上述四个函数,c++编译器将自动为A产生四个默认的函数,即:
A(void) //默认无参数构造函数
A(const A &a) //默认复制构造函数
~A(void); //默认的析构函数
A & operator = (const A &a); //默认的赋值函数
默认的复制构造函数和默认的赋值函数均采用位拷贝而非值拷贝。在类的设计当中,位拷贝是应当防止的。倘若类中含有指针变量,那么这两个缺省的函数就会发生错误。这就涉及到深复制和浅复制的问题了。
注意:
如果没定义复制构造函数,编译器会自动生成默认复制构造函数。
如果定义了其他构造函数(包括复制构造函数),编译器绝不会生成默认构造函数。
即使自己写了析构函数,编译器也会自动生成默认析构函数。
拷贝构造函数
拷贝构造函数是一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数。为啥形参必须是对该类型的引用呢?试想一下,假如形参是该类的一个实例,由于是传值参数,我们把形参复制到实参会调用拷贝构造函数,如果允许拷贝构造函数传值,就会在拷贝构造函数内调用拷贝构造函数,从而形成无休止的递归调用导致栈溢出。
深拷贝,浅拷贝
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。
但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。指向不同的内存空间,但内容是一样的
简而言之,当数据成员中有指针时,必须要用深拷贝。
#include <iostream>
using namespace std;
class String
{
public:
String(void);
String(const String &other);
~String(void);
String & operator =(const String &other);
private:
char *m_data;
int val;
};
如果定义两个String类对象,当利用位拷贝时,a=b
,其中,a.val=b.val;
是没有问题的,但是a.m_data=b.m_data
,a.m_data和b.m_data指向同一个区域。这样出现问题:
- a.m_data原来的内存区域未释放,造成内存泄露
- a.m_data和b.m_data指向同一块区域,任何一方改变,会影响到另一方
- 当对象释放时,b.m_data会释放掉两次
完整的CString类定义
class String
{
public:
String(const char *str);
String(const String &other);
String & operator=(const String &other);
~String(void);
private:
char *m_data;
};
String::String(const char *str)
{
if (str == NULL)
{
m_data = new char[1];
*m_data = '\0';
}
else
{
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}
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::operator=(const String &other)
{
if (this != &other)
{
String temp(other);
char * ptemp = other.m_data;
other.m_data = m_data;
m_data = ptemp;
}
return *this;
}
String::~String(void)
{
delete [] m_data;
}
构造函数
当创建一个类类型对象时,类通过一个或者几个特殊的成员函数来控制对象的初始化,这种函数就是构造函数。它的任务就 是用来初始化类对象的成员的,所以当创建类对象或者类对象被创建就会调用构造函数。
构造函数的几个特点:
- 函数名和类名必须一样,没有返回值。
- 当没有显式的定义构造函数时,系统会自己生成默认的构造函数。
- 构造函数可以重载(可以带多个参数,析构函数不可以重载,因为析构函数无参)
- 不可以为虚函数。
初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:
class A
{
public:
A(string s, int i):name(s), id(i){} ;
private:
string name ;int id ;
};
构造函数执行的两个阶段
从概念上来讲,构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段。
初始化阶段
所有类类型(class type)的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中。
计算阶段
一般用于执行构造函数体内的赋值操作。
使用初始化列表的原因
初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。
主要是性能问题,对于内置类型,如int
, float
等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,尤其对于数据密集型的类来说,是非常高效的。
class Test1
{
public:
Test1() {cout << "Construct Test1" << endl;}
Test1(const Test1& t1) {cout << "Copy constructor for Test1" << endl; this->a = t1.a;}
Test1& operator = (const Test1& t1)
{
cout << "assignment for Test1" << endl;
this->a = t1.a;
return *this;
}
int a;
};
class Test2
{
public:
Test1 test1;
Test2(Test1 &t1)
{
test1 = t1;
}
};
输出:
Construct Test1
Construct Test1
assignment for Test1
如果使用初始化列表的形式:
class Test2
{
public:
Test1 test1;
Test2(Test1 &t1) :test1(t1){}
};
输出:
Construct Test1
Copy constructor for Test1
必须采用列表初始化的情况:
- 成员是const类型。
- 成员是引用类型。
const对象或引用只能初始化但是不能赋值。构造函数的函数体内只能做赋值而不是初始化,因此初始化const对象或引用的唯一机会是构造函数函数体之前的初始化列表中。
从无到有叫初始化,初始化(调用拷贝构造函数)创建了新对象;赋值(调用赋值操作符)没有创建新对象,而是对已有的对象赋值。 - 类成员为没有默认构造函数的类类型
class Base
{
public:
Base(int a) : val(a) {}
private:
int val;
};
class A
{
public:
A(int v) : p(v), b(v) {}
void print_val() { cout << "hello:" << p << endl;}
private:
int p;
Base b;
};
int main(int argc ,char **argv)
{
int pp = 45;
A b(pp);
b.print_val();
}
创建对象时,要初始类成员的每一个成员(如果没有在初始化列表里面,编译器会自动使用它的默认的构造函数进行初始化,但是它没有默认构造函数,所以会编译报错,所以没有默认构造函数的成员变量需要使用初始化列表进行初始化)。
- 派生类必须在其初始化列表中调用基类的构造函数
class Base
{
public:
Base(int a) : val(a) {}
private:
int val;
};
class A : public Base
{
public:
A(int v) : p(v), Base(v) {}
void print_val() { cout << "hello:" << p << endl;}
private:
int p;
};
int main(int argc ,char **argv)
{
int pp = 45;
A b(pp);
b.print_val();
}
成员变量的初始化顺序
成员是按照他们在类中出现的顺序进行初始化的,而不是按照他们在初始化列表出现的顺序初始化的
class foo
{
public:
int i ;int j ;
foo(int x):i(x), j(i){};
};
上面的程序是正确的,先初始化i
,后初始化j
。
析构函数
类的析构函数,它是类的一个成员函数,名字由波浪号加类名构成,是执行与构造函数相反的操作:释放对象使用的资源,并销毁非static成员。
- 函数名是在类名前加上~,无参数且无返回值。
- 一个类只能有且有一个析构函数,如果没有显式的定义,系统会生成一个缺省的析构函数(合成析构函数)
- 析构函数不能重载。每有一次构造函数的调用就会有一次析构函数的调用。
- 在析构函数中,首先执行函数体,然后再销毁成员,并且成员按照初始化的逆序进行销毁。
注意
一般在显式的定义了析构函数的情况下,应该也把拷贝构造函数和赋值操作显式的定义。
class Date
{
public:
Date(int year=1990,int month=1,int day=1)
: _year(year),_month(month), _day(day)
{
p = new int;
}
~Date()
{
delete p;
}
private:
int _year=1990;
int _month;
int _day;
int *p;
};
成员中有动态开辟的指针成员,在析构函数中对它进行了delete
,如果不显式的定义拷贝构造函数,这样:Date d2(d1)
来创建d2
,我们都知道默认的拷贝构造函数是浅拷贝,那么这么做的结果就会是d2
的成员p
和d1``的p
是指向同一块空间的,呢么调用析构函数的时候回导致用一块空间被释放两次,程序会崩溃。
虚析构函数
当派生类(derived class)对象由一个基类(base class)指针删除时,若基类有一个非虚函数(non-virtual)的析构函数时,其结果是未定义的——实际执行时通常发生的是对象的派生类部分没有被销毁。
class Shape
{
public:
Shape() {
cout << "construct: shape" << endl;
}
~Shape() {
cout << "deconstruct: shape" << endl;
}
};
class Player
{
public:
Player() {
cout << "construct: player" << endl;
}
~Player() {
cout << "deconstruct: player" << endl;
}
};
class Ball
{
public:
Ball() {
cout << "construct: ball" << endl;
}
~Ball() {
cout << "deconstruct: ball" << endl;
}
private:
Shape shape_;
};
class Football : public Ball
{
public:
Football() {
cout << "construct: football" << endl;
}
~Football() {
cout << "deconstruct: football" << endl;
}
private:
Player players_;
};
int main()
{
Ball *ball = new Football();
delete ball;
return 0;
}
输出:
construct: shape
construct: ball
construct: player
construct: football
deconstruct: ball
deconstruct: shape
可以看到,当基类指针指向派生类对象时,在删除对象时,并没有调用派生类成员对象及派生类自身的析构函数,而只是调用了基类成员对象及基类的析构函数,于是就造成了“局部销毁”对象的现象,从而导致内存泄露,正确的做法是为基类指定一个虚析构函数 。
将上面的程序修改为:
class Ball
{
public:
Ball() {
cout << "construct: ball" << endl;
}
virtual ~Ball() {
cout << "deconstruct: ball" << endl;
}
private:
Shape shape_;
};
则结果变为:
construct: shape
construct: ball
construct: player
construct: football
deconstruct: football
deconstruct: player
deconstruct: ball
deconstruct: shape
是否可以为每个类都声明一个虚析构函数
class Point
{
public:
Point(int x, int y);
~Point();
private:
int x_, y_;
};
上述Point
类在32-bit机器上所占用的内存空间为8字节。若我们将Point类的析构函数指定为析函数,那么Point
类不得不提供一个vptr
(即,virtual table pointer)指针,它指向一个由函数指针构成的数组(vtbl, virtal table
)。每个带有虚函数的类都有一个相应的vtbl
。当对象调用某个虚函数时,实际被调用的函数取决于该对象的vptr
所指向的那个vtbl
。因此,无端的将所有类的析构函数声明为虚函数将导致占用内存增大,也是不合理的。
小结
- 带多态性质的基类应该声明一个虚析构函数,如果类中包含其他虚函数,也应该拥有一个虚析构函数。
- 若类的设计目的不是作为基类使用,或不是为了具备多态性,就不应该声明虚析构函数。
虚函数
用virtual
修饰的成员函数称为虚函数。
多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
- 编译时多态性:通过重载函数实现
- 运行时多态性:通过虚函数实现。
虚函数
- 虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
虚函数表
- 编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。
- 类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N4(x64下是N8)的大小。
- 派生类的虚函数表存放重写的虚函数,当基类的指针指向派生类的对象时,调用虚函数时都会根据vptr(虚表指针)来选择虚函数,而基类的虚函数在派生类里已经被改写或者说已经不存在了,所以也就只能调用派生类的虚函数版本了.
虚表指针
虚表指针在类对象中,每个同类对象中都有个一个vptr,指向内存中的vtable,所有同类对象,共享一个vtable,但是每个对象都自带一个vptr指向这个vtable,否则调用虚函数的时候会找不到正确的函数入口,虚表指针是对象的第一个数据成员。
重写(覆盖):
当在子类中定义了一个与父类完全相同的虚函数时,则称这个子类的函数重写(或覆盖)了父类的函数。
class A
{
public:
virtual void f()
{
cout << "A::f()" << endl;
}
};
class B : public A
{
public:
virtual void f()
{
cout << "B::f()" << endl;
}
};
int main()
{
A a;
B b;
a.f();
b.f();
return 0;
}
注意:
- 完全相同指函数名、参数列表和返回值相同;
- 存在一种特殊情况:协变,子类虚函数和父类虚函数的返回值分别为子类指针和父类指针;
- 只有类的成员函数才能定义为虚函数;
- 静态成员函数不能定义为虚函数,因为
static
成员函数不属于任何对象; - 如果在类外定义虚函数,只能在声明函数时加
virtual
,类外定义函数时不能加virtual
; - 构造函数不能为虚函数,虽然可以将
operator=
定义为虚函数,但最好不要这样做,因为使用时容易引起混淆; - 内联函数不能为虚函数,如果内联函数被
virtual
修饰,计算机会忽视inline
将它变成纯粹的虚函数; - 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为;
- 最好把基类的析构函数定义为虚函数;(如果没有定义为虚函数,当基类指针指向派生类,并且删除指针时,会析构基类而不会析构派生类,造成内存泄漏)
虚函数实现原理
虚函数(Virtual Function)
是通过一张虚函数表(Virtual Table)
来实现的。简称为V-Table
。它是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。
在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
int _tmain(int argc, _TCHAR* argv[])
{
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
pFun = (Fun)*((int*)*(int*)(&b)+1);
pFun();
pFun = (Fun)*((int*)*(int*)(&b)+2);
pFun();
return 0;
}
我们可以通过强行把&b
转成int *
,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f()*
。
一般继承(无虚函数覆盖)
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
- 虚函数按照其声明顺序放于表中。
- 父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
- 覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
- 没有被覆盖的函数依旧。
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()
函数地址所取代,于是在实际调用发生时,是Derive::f()
被调用了,这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
- 每个父类都有自己的虚表。
- 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下面是对于子类实例中的虚函数表的图:
- 三个父类虚函数表中的f()的位置被替换成了子类的函数指针。
class Base1 {
public:
virtual void f() { cout << "Base1::f" << endl; }
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
virtual void f() { cout << "Base2::f" << endl; }
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
};
class Derive :public Base1, public Base2, public Base3
{
virtual void f() { cout << "Derive::f" << endl; }
virtual void g1() { cout << "Derive::g1" << endl; }
};
int _tmain(int argc, _TCHAR* argv[])
{
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f();
b2->f();
b3->f();
b1->g();
b2->g();
b3->g();
return 0;
}
总结:
- 单继承
子类和父类有各自的虚表,子类虚表拷贝父类虚表中的内容,并会更新构成覆盖的函数地址。 - 多继承
子类中包含多个虚表(取决于继承的个数),各自拷贝父类虚表中的内容,并更新构成覆盖的函数地址,对于子类中没有构成覆盖的虚函数,将其地址添加到最先继承类的虚表中。
纯虚函数
在虚函数后面赋值0。
class A
{
virtual void func() = 0; // 纯虚函数
protected :
string _a ;
};
class B : public A
{};
注意:
- 纯虚函数没有函数体。
- 最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是虚函数”。
- 这是一个声明语句,最后有分号。
- 纯虚函数只有函数的名字而不具备函数的功能,不能被调用。
- 纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义。如果在基类中没有保留函数名字,则无法实现多态性。
- 如果在一个类中声明了纯虚函数,在其派生类中没有对其函数进行定义,则该虚函数在派生类中仍然为纯虚函数。
抽象类(接口类)
含有纯虚函数的类称为抽象类(接口类),抽象类不能实例化出对象。
抽象类的存在,使得子类必须重写虚函数才能实例化出对象。
- 凡是包含纯虚函数的类都是抽象类。
- 抽象类不能实例化出对象。
- 纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
继承
钻石继承
在C++中,类是允许多继承的,多继承大大的提高了代码的复用、减少代码冗余、大大的提高了类的表现力,使得类更贴近现实中的事物,使用起来更为灵活,更面向对象。
钻石继承是多继承的一种情况如下图:类A中派生出类X 和类Y ,类X和类Y派生出类Z,那么类A称为公共基类,类Z称为汇合子类。
class A
{
public:
A(int data) : m_data(data)
{
cout << "A构造 : " << this << endl;
}
protected:
int m_data;
};
class X : public A
{
public:
X(int data) : A(data)
{
cout << "X构造 : " << this << endl;
}
int getData(void) const
{
return m_data;
}
};
class Y : public A
{
public:
Y(int data) : A(data)
{
cout << "Y构造 : " << this << endl;
}
void setData(int data)
{
m_data = data;
}
};
class Z : public X, public Y
{
public:
Z(int data) : X(data), Y(data)
{
cout << "Z构造 : " << this << endl;
}
};
int main(void)
{
Z z(0);
z.setData(100);
cout << "m_data = " << z.getData() << endl;
return 0;
}
首先发现了公共基类 A中的m_data并没有成功,而且从打印信息中发现类A实例化了2次,而且打印出来的地址分别和X和Y的地址一样。这说明了Z的实例中存在了2份A的实例,分别存在于实例Y和实例X中。
在z.getData
中,是通过实例X提供的函数访问了实例X中的实例A。
在z.setData
中,是通过实例Y提供的函数修改了实例Y中的实例A。
这说明了Z的实例通过不同的路径访问实例A,得到了不一样的数据,这样并没有达到设计中的要求,而且使用起来也很不人性化。
小结
- 派生多个中间子类的公共基类子对象,在继承自多个中间子类的汇聚子类对象中,存在多个实例。
- 在汇聚子类中,或通过汇聚子类对象,访问公共基类的成员,会因继承路径的不同而导致不一致。
- 通过虚继承,可以保证公共基类子对象在汇聚子类对象中,仅存一份实例,且为多个中间子类子对象所共享。
虚继承
为了令Z的实例中只拥有一份A的实例,可以采取虚继承来解决钻石继承带来的问题。
在继承表中使用virtual关键字。
class A
{
public:
A(int data) : m_data(data)
{
cout << "A构造 : " << this << endl;
}
protected:
int m_data;
};
class X : virtual public A
{
public:
X(int data) : A(data)
{
cout << "X构造 : " << this << endl;
}
int getData(void) const
{
return m_data;
}
};
class Y : virtual public A
{
public:
Y(int data) : A(data)
{
cout << "Y构造 : " << this << endl;
}
void setData(int data)
{
m_data = data;
}
};
class Z : public X, public Y
{
public:
Z(int data) : X(data), Y(data), A(data)
{
cout << "Z构造 : " << this << endl;
}
};
int main(void)
{
Z z(0);
z.setData(100);
cout << "m_data = " << z.getData() << endl;
return 0;
}
通过虚继承问题就能解决了,类A只构造了一次,所以在类Z的实例中只存在一份实例A。
虚指针和虚表
通过虚继承方式被继承的基类称为虚基类,通过虚继承派生的子类,会拥有一个虚指针,该指针指向一个虚表,虚表中记录的该类的各种信息,例如实例中与虚基类实例的偏移量,和虚函数与普通函数的入口地址。
虚指针和虚表在C++实现多态的实现中起到重要的作用。
在代码中,A是Z的虚基类子对象,X和Y相对于Z是中间基类子对象。
内存模型:虚指针->虚基类表->虚基类子对象相对于中间基类子对象的偏移量。
这样在运行期间就能通过虚指针访问虚表,在从虚表中取得偏移量,就能通过X和Y访问到唯一的A了。
小结
- 位于继承链最末端的子类的构造函数负责构造虚基类子对象。
- 虚基类的所有子类(无论直接的还是间接的)都必须在其构造函数中显式指明该虚基类子对象的构造方式,否则编译器将选择以缺省方式构造该子对象。
- 虚基类的所有子类(无论直接的还是间接的)都必须在其拷贝构造函数中显式指明以拷贝方式构造该虚基类子对象,否则编译器将选择以缺省方式构造该子对象。
智能指针
智能指针的作用
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
shared_ptr
shared_ptr
多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
- 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如
std::shared_ptr<int> p4 = new int(1);
的写法是错误的 - 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
- get函数获取原始指针
#include <iostream>
#include <memory>
int main() {
{
int a = 10;
std::shared_ptr<int> ptra = std::make_shared<int>(a);
std::shared_ptr<int> ptra2(ptra);
std::cout << ptra.use_count() << std::endl; //2
std::cout << ptra2.use_count() << std::endl; //2
int b = 20;
int *pb = &a;
std::shared_ptr<int> ptrb = std::make_shared<int>(b);
ptra2 = ptrb;
pb = ptrb.get(); //获取原始指针
std::cout << ptra.use_count() << std::endl; //1
std::cout << ptrb.use_count() << std::endl;//2
}
}
unique_ptr
unique_ptr
“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr
指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。
unique_ptr
指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr
指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset
方法重新指定、通过release
方法释放所有权、通过移动语义转移所有权。
int main() {
{
std::unique_ptr<int> uptr(new int(10)); //绑定动态对象
//std::unique_ptr<int> uptr2 = uptr; //不能賦值
//std::unique_ptr<int> uptr2(uptr); //不能拷貝
std::unique_ptr<int> uptr2 = std::move(uptr); //转换所有权
uptr2.release(); //释放所有权
}
//超过uptr的作用域,內存释放
}
weak_ptr的使用
weak_ptr
是为了配合shared_ptr
而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*
和->
,它的最大作用在于协助shared_ptr
工作,像旁观者那样观测资源的使用情况。
weak_ptr
可以从一个shared_ptr
或者另一个weak_ptr
对象构造,获得资源的观测权。但weak_ptr
没有共享资源,它的构造不会引起指针引用计数的增加。
使用weak_ptr
的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()
的功能等价于use_count()==0
,但更快,表示被观测的资源(也就是shared_ptr
的管理的资源)已经不复存在。
weak_ptr
可以使用一个非常重要的成员函数lock()
从被观测的shared_ptr
获得一个可用的shared_ptr
对象, 从而操作资源。但当expired()==true
的时候,lock()
函数将返回一个存储空指针的shared_ptr
。
int main() {
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl; // 1
std::weak_ptr<int> wp(sh_ptr);
std::cout << wp.use_count() << std::endl;// 1
if (!wp.expired()){
std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl;// 2
}
}
}
智能指针的设计和实现
#include <iostream>
#include <memory>
template<typename T>
class SmartPointer {
private:
T* _ptr;
size_t* _count;
public:
SmartPointer(T* ptr = nullptr) :
_ptr(ptr) {
if (_ptr) {
_count = new size_t(1);
}
else {
_count = new size_t(0);
}
}
SmartPointer(const SmartPointer& ptr) {
if (this != &ptr) {
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
}
}
SmartPointer& operator=(const SmartPointer& ptr) {
if (this->_ptr == ptr._ptr) {
return *this;
}
if (this->_ptr) {
(*this->_count)--;
if (this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
return *this;
}
T& operator*() {
assert(this->_ptr == nullptr);
return *(this->_ptr);
}
T* operator->() {
assert(this->_ptr == nullptr);
return this->_ptr;
}
~SmartPointer() {
(*this->_count)--;
if (*this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
size_t use_count(){
return *this->_count;
}
};
STL
STL的介绍
Standard Template Library
,标准模板库,是C++的标准库之一,一套基于模板的容器类库,还包括许多常用的算法,提高了程序开发效率和复用性。STL包含6大部件:容器、迭代器、算法、仿函数、适配器和空间配置器。
- 容器:容纳一组元素的对象。
- 迭代器:提供一种访问容器中每个元素的方法。
- 函数对象:一个行为类似函数的对象,调用它就像调用函数一样。
- 算法:包括查找算法、排序算法等等。
- 适配器:用来修饰容器等,比如queue和stack,底层借助了deque。
- 空间配置器:负责空间配置和管理。
各种容器的特性
vector | 典型的序列容器,C++标准严格要求次容器的实现内存必须是连续的,唯一可以和标准C兼容的stl容器,任意元素的读取、修改具有常数时间复杂度,在序列尾部进行插入、删除是常数时间复杂度,但在序列的头部插入、删除的时间复杂度是O(n),可以 在任何位置插入新元素,有随机访问功能,插入删除操作需要考虑。 |
---|---|
deque | 序列容器,内存也是连续的,和vector相似,区别在于在序列的头部插入和删除操作也是常数时间复杂度, 可以 在任何位置插入新元素,有随机访问功能。 |
list | 序列容器,内存是不连续的,任意元素的访问、修改时间复杂度是O(n),插入、删除操作是常数时间复杂度, 可以 在任何位置插入新元素。 |
set | 关联容器,元素不允许有重复,数据被组织成一棵红黑树,查找的速度非常快,时间复杂度是O(logN) |
multiset | 关联容器,和set一样,却别是允许有重复的元素,具备时间复杂度O(logN)查找功能。 |
map | 关联容器,按照{键,值}方式组成集合,按照键组织成一棵红黑树,查找的时间复杂度O(logN),其中键不允许重复。 |
multimap | 和map一样,区别是键可以重复 |
vector
- vector底层是一个动态数组,包含三个迭代器,
start
和finish
之间是已经被使用的空间范围,end_of_storage
是整块连续空间包括备用空间的尾部。 - vector内存增长机制:当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间。
- size表示当前vector中有多少个元素(finish - start),而capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start)。
- vector的底层实现要求连续的对象排列,引用并非对象,没有实际地址,因此vector的元素类型不能是引用。
vec.clear()
:清空内容,但是不释放内存。vector<int>().swap(vec)
:清空内容,且释放内存,想得到一个全新的vector。- 常用操作
vector<int> vec(10,100); 创建10个元素,每个元素值为100
reverse(vec.begin(),vec.end()) 将元素翻转
sort(vec.begin(),vec.end()); 排序,默认升序排列
vec.push_back(val); 尾部插入数字
vec.size(); 向量大小
find(vec.begin(),vec.end(),1); 查找元素
iterator = vec.erase(iterator) 删除元素
list
list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。
list的常用函数
list.push_back(elem) 在尾部加入一个数据
list.pop_back() 删除尾部数据
list.push_front(elem) 在头部插入一个数据
list.pop_front() 删除头部数据
list.size() 返回容器中实际数据的个数
list.sort() 排序,默认由小到大
list.unique() 移除数值相同的连续元素
list.back() 取尾部迭代器
list.erase(iterator) 删除一个元素,参数是迭代器,返回的是删除迭代器的下一个位置
deque
deque是一个双向开口的连续线性空间(双端队列),在头尾两端进行元素的插入跟删除操作都有理想的时间复杂度。
deque的常用函数
deque.push_back(elem) 在尾部加入一个数据。
deque.pop_back() 删除尾部数据。
deque.push_front(elem) 在头部插入一个数据。
deque.pop_front() 删除头部数据。
deque.size() 返回容器中实际数据的个数。
deque.at(idx) 传回索引idx所指的数据,如果idx越界,抛出out_of_range。
vector、list、deque使用场景
- vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
- list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
- 需要从首尾两端进行插入或删除操作的时候需要选择deque。
priority_queue
priority_queue
:优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。
priority_queue的常用函数
priority_queue<int, vector<int>, greater<int>> pq; 最小堆
priority_queue<int, vector<int>, less<int>> pq; 最大堆
pq.empty() 如果队列为空返回真
pq.pop() 删除对顶元素
pq.push(val) 加入一个元素
pq.size() 返回优先队列中拥有的元素个数
pq.top() 返回优先级最高的元素
map 、set、multiset、multimap
map 、set、multiset、multimap的底层实现都是红黑树。
红黑树的特性:
- 每个结点或是红色或是黑色;
- 根结点是黑色;
- 每个叶结点是黑的;
- 如果一个结点是红的,则它的两个儿子均是黑色;
- 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。
对于STL里的map容器,count方法与find方法,都可以用来判断一个key是否出现,mp.count(key) > 0
统计的是key出现的次数,因此只能为0/1,而mp.find(key) != mp.end()
则表示key存在。
map 、set、multiset、multimap的特点
set
和multiset
会根据特定的排序准则自动将元素排序,set
中元素不允许重复,multiset
可以重复。map
和multimap
将key和value组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以map
默认是按key排序的),map
中元素的key不允许重复,multimap可以重复。map
和set
的增删改查速度为都是logn
,是比较高效的。
map 、set、multiset、multimap的常用函数
it map.begin() 返回指向容器起始位置的迭代器(iterator)
it map.end() 返回指向容器末尾位置的迭代器
bool map.empty() 若容器为空,则返回true,否则false
it map.find(k) 寻找键值为k的元素,并用返回其地址
int map.size() 返回map中已存在元素的数量
map.insert({int,string}) 插入元素
for (itor = map.begin(); itor != map.end();)
{
if (itor->second == "target")
map.erase(itor++) ; // erase之后,令当前迭代器指向其后继。
else
++itor;
}
unordered_map、unordered_set
unordered_map的底层是一个防冗余的哈希表(采用除留余数法)。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。
unordered_map、unordered_set的常用函数
unordered_map.begin() 返回指向容器起始位置的迭代器(iterator)
unordered_map.end() 返回指向容器末尾位置的迭代器
unordered_map.cbegin() 返回指向容器起始位置的常迭代器(const_iterator)
unordered_map.cend() 返回指向容器末尾位置的常迭代器
unordered_map.size() 返回有效元素个数
unordered_map.insert(key) 插入元素
unordered_map.find(key) 查找元素,返回迭代器
迭代器的种类
- 输入迭代器:是只读迭代器,在每个被遍历的位置上只能读取一次。例如上面find函数参数就是输入迭代器。
- 输出迭代器:是只写迭代器,在每个被遍历的位置上只能被写一次。
- 前向迭代器:兼具输入和输出迭代器的能力,但是它可以对同一个位置重复进行读和写。但它不支持operator–,所以只能向前移动。
- 双向迭代器:很像前向迭代器,只是它向后移动和向前移动同样容易。
- 随机访问迭代器:有双向迭代器的所有功能。而且,它还提供了“迭代器算术”,即在一步内可以向前或向后跳跃任意位置, 包含指针的所有操作,可进行随机访问,随意移动指定的步数。支持前面四种Iterator的所有操作,并另外支持
it + n、it - n、it += n、it -= n、it1 - it2
和it[n]
等操作。
进程与线程
线程的基本概念
线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
线程的状态
状态:运行、阻塞、挂起阻塞、就绪、挂起就绪
状态之间的转换:
- 准备就绪的进程,被CPU调度执行,变成运行态;
- 运行中的进程,进行I/O请求或者不能得到所请求的资源,变成阻塞态;
- 运行中的进程,进程执行完毕(或时间片已到),变成就绪态;
- 将阻塞态的进程挂起,变成挂起阻塞态,当导致进程阻塞的I/O操作在用户重启进程前完成(称之为唤醒),挂起阻塞态变成挂起就绪态,当用户在I/O操作结束之前重启进程,挂起阻塞态变成阻塞态;
- 将就绪(或运行)中的进程挂起,变成挂起就绪态,当该进程恢复之后,挂起就绪态变成就绪态;
线程与进程的区别
进程和线程的关系
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- 处理机分给线程,即真正在处理机上运行的是线程。
- 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体。
进程与线程的区别
- 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
- 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
- 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
多线程的优缺点
优点
- 多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态。
- 占用大量处理时间的任务使用多线程可以提高CPU利用率,即占用大量处理时间的任务可以定期将处理器时间让给其它任务。
- 多线程可以分别设置优先级以优化性能。
缺点
- 等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源 ,如打印机等。
- 对线程进行管理要求额外的 CPU开销,线程的使用会给系统带来上下文切换的额外负担。
- 线程的死锁。即对共享资源加锁实现同步的过程中可能会死锁。
- 对公有变量的同时读或写,可能对造成脏读等。
进程间通信的方式
- 管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
- 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
- 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
- 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
- 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
- 套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
多线程同步和互斥
当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。
同步
所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
互斥
所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
多线程同步和互斥实现方法
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。