预处理器

C预处理器处理程序的源代码,在编译器运行之前运行,通常以符号"#"开头。

C预言的预处理主要有三个方面的内容:

  1. 宏定义与宏替换
  2. 文件包含
  3. 条件编译

  

宏定义与宏替换

  “宏”是借用汇编预言中的概念,为的是在C语言程序中方便的做一些定义和扩展。这些语句以“#define”开头,分为两种:符号常量的宏定义和带参数的宏定义

  • 符号常量的宏定义和宏替换
1
#define 标识符 字符串

  其中标识符就是宏名称,注意宏定义末尾不加分号.

  由于预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,所以预处理是不做语法检查的。且宏定义不分配内存,变量定义才分配内存。

  • 带有参数的宏定义及其替换

  对带有参数的宏定义进行宏替换时,不仅对宏标识符作字符串替换,还必须作参数的替换。有时为了避免发生错误,需要在宏参数上加括号。

1
2
3
4
5
6
#define 标识符(参数列表) 字符串


#define FUN(x) (x*x) //FUN(a+b)将被替换为a+B*a+B

#define FUN(x) ((x)*(x)) //FUN(a+b)将被替换为(a+B)*(a+B)

  宏替换的本质就是文本替换,需要注意:

  1. 宏名一般用大写,宏名和参数的括号间不能有空格,宏定义末尾不加分号;
  2. 宏替换只做替换,不做语法检查、不做计算、不做表达式求解;
  3. 宏替换在编译前进行,不分配内存;函数调用在编译后的程序运行时进行,且分配内存;
  4. 函数只有一个返回值,利用宏则可以设法得到多个值;
  5. 宏替换会使源程序变长,函数调用不会;
  6. 宏替换不占用运行时间,只占用编译时间;函数调用占用运行时间(内存分配、保留现场、值传递、返回值)

  实际工程中应尽量少用宏替换。C++中宏替换实现的符号常量功能由const、enum代替,带参数的宏替换可由模板内联函数代替。

文件包含

1
2
#include <func.h>
#include "func.h"

  如果头文件名在尖括号<>中,那么认为该头文件是标准头文件。编译器将会在预定义的位置集合中查找该头文件,这些预定义的位置可以通过设置查找路径和环境变量或者修改命令行选项来修改;

  如果头文件在一对引号中,则认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径中;

条件编译

  提供条件编译措施使得同一源程序可以根据不同编译条件(参数)选择不同的目标代码,其作用在于便于调试和移植。

1
2
3
4
#if/ifdef/ifndef
#elif
#else
#endif

全局变量和局部变量

  全局变量也称为外部变量,在函数的外部定义。它属于一个源程序文件,作用域是整个源程序。

  在不同的文件中引用一个已经定义过的全局变量,可以用头文件引用的方式,也可以用extern关键字。假设变量写错了,如果使用头文件包含的方式,那么编译期间会报错;如果使用extern关键字,编译期间不会报错,链接期间报错。

1
2
3
4
5
6
//file_1.cpp
int counter;

//file_2.cpp
extern int counter; //使用file_1中的counter
++counter;

  局部变量指在程序中,只在特定过程或函数中可以访问的变量,是相对全局变量而言的。在面向过程的语言中,局部变量可以和全局变量重名,但局部变量会屏蔽全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int count = 3;  //语句1

int main()
{
int i, sum, count = 2; //语句2
for(i = 0;i < count; i+=2, count++) //判断条件count是语句2的count
{
static int, count = 4; //语句3
count++; //用的是语句3的count
if(i % 2 == 0)
{
extern int count;
count++; //因为用了extern,这里的count是语句1的count
sum += count; //因为用了extern,这里的count是语句1的count
}
sum += count; //语句3的count
}

printf("%d %d\n", count, sum); //语句2的count
return 0;
}

static

普通的static

普通的static作用有三个:

  1. 隐藏:当我们同时编译多个文件时,所有未加static的全局变量和函数都具有全局可见性,其他的源文件也可以访问。如果加了static前缀,则全局变量和函数对其他源文件隐藏,只在当前文件生效。
  2. 默认初始化为0,包括未初始化的全局静态变量和局部静态变量。全局未初始化的变量本身也具备这个属性,未初始化的全局变量和未初始化的静态变量都在BSS段内,所有的字节默认为0x00;
  3. 保持局部变量内容的持久。函数内的普通的局部变量退出作用域就消失了,但静态的局部变量退出函数后仍然存在,生存周期为整个源程序的周期。static的局部变量特点是只进行一次初始化且具有“记忆性”。不过虽然生存周期是整个程序,但作用域仍然和普通局部变量相同,出了函数虽然还存在,但不能使用。

类中的static

  C++重用了static关键字,并赋予了不同的含义:类中的static表示属于一个类但不属于此类的任何特定对象的变量和函数。static的成员变量和成员函数都独立于类对象存在。

  • 静态数据成员

  普通数据成员存在于该类的每个对象中,但static数据成员独立于该类的任意对象存在:static数据成员是与类关联的对象,二不与该类的对象相关联。当某个类的实例修改了该静态成员变量,修改后的值被该类的所有实例所见。

  静态数据成员和普通数据成员一样,也遵从public、protected、private的访问规则。

  静态数据成员也存储在全局(静态)存储区。静态数据成员定义时要分配空间,所以不能在类声明中定义,必须在类定义体的外部定义。

1
2
3
4
5
6
7
8
9
10
11
class Account
{
public:
void apply();
static double rate() {return interestRate;}
private:
std::string owner;
static double interestRate;
};

double Account::interestRate = 10; //类外定义

使用static成员变量而不是全局变量有三个优点:

  1. static成员的名字还在类的作用域中,可以避免全局冲突;
  2. static成员可以声明为私有的,实施封装;
  3. 阅读方便,可以明确看出static成员与类是关联的

  • 静态成员函数

  和静态成员变量一样,静态成员函数是类的内部实现,属于类定义的一部分,为类服务而不是为类的具体对象服务。

  普通成员函数服务于具体的对象,所以普通的成员函数都隐含了一个this指针指向类的对象本身;静态成员函数不与对象关联,所以不具有this指针。所以静态成员函数无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,只能调用类其余的静态成员函数与访问静态数据成员。不过非静态的成员函数可以任意访问静态的成员函数和变量。由于没有this的开销,静态成员函数相比普通的成员函数速度上有少许增长。

  static成员变量可以被声明为const,但static成员函数不可以。const成员函数的意思是承诺不修改该函数所属的对象,但static成员函数不属于任何对象。

  static成员函数也不能被声明为虚函数、volatile。

const

常量

  C++中const限定符把一个对象转换为常量,常量定义后不允许修改,所以定义时必须初始化。

1
const int bufSize = 512; //必须初始化

  全局变量在整个程序中都可以访问,但全局的const变量是定义该变量的文件的局部变量。如果想被其他文件访问,需要声明extern。

  C和C++中的const有区别。常量引进是在早期的C++版本中,当时标准C规范正在制定。C中的const意思是“一个不能被改变的普通变量”,在C中总是占用存储,C编译器不能把const视为一个编译器的常量:

1
2
3
//以下写法在C中是错误的,C++中是允许的
const bufSize = 100;
int buf[bufSize];

  C默认const是外部连接的,C++默认const是内部连接的,如果想在C+中完成于C同样的事情,需要加extern变成外部连接:

1
2
3
4
5
//C语言可以这么写,C编译器只把他当作声明,C++编译器必须初始化,不能这么写
const int size;

//C++必须加extern
extern const bufSize; //只声明

  const最初的动机是取代#define的值替换功能,使用const取代#define的优点如下:

  1. const常量有数据类型,宏常量没有数据类型,所以编译器可以对const常量进行类型安全检查;
  2. 预处理会盲目的将宏常量进行代码替换,可能导致代码中多出很多个备份,所以使用const常量可能比使用#define产生更小的目标代码;
  3. const可以执行常量折叠:常量折叠是编译期间简化常量表达式的过程,简单说就是将常量表达式计算求值,并用求得的值替换表达式放入常量表。

指针和const

  需要区分指向const的指针和const指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//指向const的指针:
const double *cptr;
// cptr是个指针,它指向一个const double
// cptr可以指向任何东西,所以不需要初始化,但是它所指的东西不能改变


//const指针
//使指针成为const,要把const标明的部分放在*右边
double d = 1.0;
double* const cptr = &d;
// cptr是个指针,这个指针是指向double的const指针
// 指针本身是const,所以必须初始化
// 在指针寿命期间内,指向的地址不可以改变,但地址里的内容可以变更

const修饰函数参数和返回值

  在一个函数声明式内,const可以和函数返回值、各参数、类成员函数函数自身产生关联:

  • const修饰返回值

  若返回值是值类型,则对于内部数据类型来说,返回值是否是常量并没有关系,const修饰返回值通常用于处理用户定义的类型;除了值类型,还可以返回指针。正常的函数不能返回指向局部变量的指针,因为函数返回后指针就无效了,栈也被情理了,但如果返回的指针是指向堆中分配的存储空间的指针或者是指向静态存储区的指针,在返回后仍然有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//错误!!
// p是数组在栈上,返回的是指向栈内存的指针,
//返回的地址不是NULL,但内容已经被清除了
char* GetMemory()
{
char p[] = "hello world";
return p;
}


//正确
//数组在静态存储区,可以通过函数返回
char* GetMemory()
{
static char p[] = "hello world";
return p;
}



//正确
//p指向全局静态存储区,可以通过函数返回
//因为p指向常量,这里最好加上const修饰
//如果不加const,函数外面如果对返回的字符串做修改就崩溃了
const char* GetMemory()
{
char p* = "hello world";
return p;
}


//不太好的写法
//p指向堆内存的地址,可以通过函数返回
//但是需要外部来delete[]释放内存
char* GetMemory()
{
char* p = (char*)malloc(12);
if(p == NULL){
return NULL;
}
else{
p = "hello world";
}
return p;
}

  • const修饰函数参数

  参数加上const可以明确告知编译器参数在函数体内部不会也无法改变。如果是值传递,加const意义不大,但如果是传递地址,那么都应该尽可能的用const修饰。

const与类

  • const成员函数
1
2
3
4
class base{
void func1();
void func2() const;
}

  被const修饰的类成员函数改变了隐含的this形参的类型,使得this形参指向的对象为const类型。this本身的类型是base* const,被const修饰后变成了const base* const。const成员函数不能修改调用该函数的对象。

  const成员函数的目的是为了确保该成员函数可以作用于const对象身上。const对象、指向const对象的指针或引用只能调用其const成员函数;非const对象可以调用所有成员函数。显然,若不存在const成员函数,那么const对象的操作就变的极为困难,无法调用任何成员函数了。

  要注意,如果类成员函数只是常量性质不同,其余都一样,是可以被重载的:

1
2
3
4
class base{
void func1();
void func1() const;
}

  • const数据成员

  如果一个类的数据成员声明为const,那么必须在构造函数的初始化列表中进行初始化,且必须具有构造函数。不过如果数据成员同时具有static和const属性,那么也可以使用外部初始化。const数据成员也不可以在类定义处初始化,因为const数据成员只是在某个对象的生存周期内是常量,对于整个类而言是可变的,不同的对象可以有不同的值,如果在类声明中初始化const数据成员,因为类对象还没有创建,编译器不知道const数据成员的值是什么。

1
2
3
4
5
6
class Thing{
public:
Thing():valueB(1){}
private:
const int valueB;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test
{
public:
Test():a(0){}
enum {
size1 = 100,
size2 = 200
};

private:
const int a; //只能在构造函数初始化列表中初始化
static int b; //在类外定义并初始化
const static int c;
//也可以写作static const int c;
//c这里是整型,所以也可以在这里初始化,但仍需要在类外进行定义
//如果c不是整形(char、short、int、long),就不允许在这里初始化
};

int Test::b = 0; //static成员变量不允许在类内初始化,不属于某个对象
const int Test::c = 0; //给const static成员变量赋值时可以不加static,但必须加const

内存管理与释放

  C/C++程序,用户主要使用的内存主要分为:栈区、堆区、全局(静态)存储区、文字常量区、代码区。

  堆区和栈区的区别:

  • 栈区:由编译器自动分配和释放,存放函数的参数值、局部变量的值等。操作方式类似于数据结构中的栈,速度较快;
  • 堆区:一般由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。要注意和数据结构中的堆是两回事,它的分配方式类似链表。一般速度比较慢,而且容易产生内存碎片,不过用起来方便。
1
2
3
4
5
6
7
//C语言
char* p1 = (char*)malloc(10);

//C++
char* p2 = new char[10];

//要注意p1和p2本身是栈上的变量,只是指向堆上分配的内存

  每一个程序执行时都占用一块可用的内存空间,用于存放动态分配的对象,这个内存空间就是堆。C语言使用标准库函数malloc和free在堆区分配存储空间,C++语言使用new和delete表达式实现。

动态创建对象的初始化

  通常,动态创建的对象如果不提供显式的初始化,那么对于类对象,用该类的默认构造函数初始化,当然也可以使用显式初始化;而内置类型的对象则无初始化;

  对于提供了默认构造函数的类的类型,没有必要对其对象进行显式初始化,操作结果一样;但如果是内置类型或者是没有默认构造函数的类型,不同的初始化方式有显著的差别:

1
2
3
4
5
6
7
8
9
//std::string类有默认构造函数
//无论是隐式的还是显式的,结果都是调用默认构造
string* ps = new string; //调用默认构造函数初始化
string* ps = new string(); //调用默认构造函数初始化


//int是内置类型的,隐式的和显式的有区别
int* pi = new int; //隐式的,无初始化
int* pi = new int(); //显式的,pi指向一个初始化为0的int值

  动态创建的对象用完后,程序员必须显式的将该对象占用的内存释放,否则就会内存泄漏,C语言提供了free函数,C++使用delete表达式。回收用new分配的单个对象的内存空间用delete,回收用new[]分配的一组对象的内存空间时用delete[];

1
2
3
char* p = new char[64];
delete[] p;
p = nullptr;

const对象的动态分配和回收

1
const int* pci = new const int(1024);

  C++允许动态创建const对象。与其他常量一样,动态创建的const对象必须在创建时初始化,且一经初始化其值不可再修改。

  对于一个类的const动态对象,如果该类提供了默认的构造函数,则此对象可以隐式初始化;内置类型对象或者未提供默认构造函数的类对象必须显式初始化:

1
2
3
4
//std::string有默认的构造函数
//new表达式没有显式的初始化pcs所指的对象
//而是隐式的将pcs所指的对象初始化为空string
const string *pcs = new const string;

  尽管程序员不能改变const对象的值,但是可以撤销对象本身。如同其他对象一样,const动态对象也可以使用指针释放:

1
2
const string *pcs = new const string;
delete pci;

new/delete和malloc/free比较

相同点:都可以用于申请动态内存和释放内存

不同点:

  1. malloc和free是C/C++语言的标准库函数,new/delete是C++的运算符;
  2. new自动计算需要分配的空间,而malloc需要手工计算字节数;
  3. new是类型安全的,malloc不是;
  4. new和delete可以调用相关对象的构造和析构,malloc和free不行;
  5. malloc和free需要库文件支持,new和delete不需要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
malloc和free由于是库函数,不在编译器控制权限之内,所以无法执行类的构造函数和析构函数;



new的执行过程:
1.operator new的标准库函数(operator new对应malloc),分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;
2.运行该类的一个构造函数,用指定初始化式构造对象
3.返回指向新分配并构造的对象的指针;

delete的执行过程:
1.首先对指针指向的对象运行适当的析构函数;
2.调用operator delete的标准库函数释放该对象用的内存(operator delete对应free);



malloc的函数原型如下:
void* malloc(size_t size);
用malloc申请一块长度为length的整数类型的内容程序如下:
int* p = (int*)malloc(sizeof(int) * length);
可见malloc返回值的类型是void*,所以在调用malloc时一定要显式类型转换;
且malloc函数本身并不识别要申请的内存类型,只关心总字节数

free函数的原型如下:
void free(void* memblock);
free(p)来释放内存,如果p是NULL,那么free(p)操作多少次也不会出问题;
但如果p不是NULL,那么连续free(p)两次就会导致程序运行错误



new内置了sizeof、类型转换和类型安全检查功能,所以使用简单
int* p2 = new int[length];

内存池

  通常我们直接使用new、malloc等申请内存,这样做有个缺点:由于所申请的内存块大小不定,当频繁使用时会造成大量的内存碎片降低性能。

  内存池是一种内存分配方式,在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。

进程

进程概念

  在没有操作系统之前,计算机只能运行一个程序,全部的资源都属于当前运行的程序。配置了操作系统后,引入了多道程序设计的概念,进程可以合理的隔离资源和运行环境、提升资源利用率。

  • 进程是系统进行资源分配和调度的基本单位;
  • 进程作为程序独立运行的载体保障程序正常执行;
  • 进程的存在使得操作系统资源的利用率大幅提升;

进程实体

  主存中的进程是一个连续的存储空间,称为进程控制块(PCB)。进程控制块是用于描述和控制进程运行的通用数据结构。进程控制块中存有进程状态、优先级、程序计数器、内存指针、上下文数据、IO状态信息、记账信息等多个进程当前状态和控制进程的信息:

  • 标识符:唯一标记一个进程,用于区别其他进程;
  • 状态:标记进程的运行状态
  • 程序计数器:指向进程即将被执行的下一条指令的地址;
  • 内存指针:程序代码、进程数据相关的指针;
  • 上下文数据:存储进程执行时处理器中的数据;
  • IO状态信息:被进程IO操作所占用的文件列表;
  • 记账信息:存储进程使用处理器的时间、时钟数总和等信息;

  除了上面的内容,进程还有一些其他信息。所有信息可以分为四大类:进程标识符、处理机状态、进程调度信息、进程控制信息。进程控制块使得进程是能够独立运行的基本单位,操作系统通过进程控制块信息来调度和控制进程。进程控制块是常驻内存的,存放在系统专门开辟的PCB区域内。

进程与线程

  进程(Process)和线程(Thread)是一对多的关系,一个进程内可以有一个或多个线程。

  进程是系统进行资源分配和调度的基本单位,而线程是操作系统进行运行调度的最小单位。线程包含在进程之中,是进程中实际运行工作的单位。一个进程可以并发多个线程,每个线程执行不同的任务。

  进程拥有资源,而线程不拥有资源,进程内部的线程共享进程资源。

进程 线程
资源 资源分配的基本单位 不拥有资源
调度 独立调度的基本单位 独立调度的最小单位
系统开销 进程系统开销大 线程系统开销小
通信 进程IPC 读写同一进程数据通信

五状态模型

  • 就绪状态

  当进程被分配到除CPU以外的所有必要资源后,就处于就绪状态。只要再获得CPU的使用权,就可以立即运行。在一个系统中多个处于就绪状态的进程通常排成一个队列,称为就绪队列。

  • 执行状态

  进程获得CPU,其程序开始执行,这个状态为执行状态。在单处理机中,某个时刻只能有一个进程是处于执行状态。

  • 阻塞状态

  进程因为某种原因,比如其他设备未就绪而无法执行,从而放弃CPU的状态称为阻塞状态。和就绪队列一样,操作系统也有阻塞队列,存放所有阻塞的进程。

  • 创建状态

  进程创建分为两步,第一步分配PCB,第二步插入就绪队列。创建进程时拥有PCB但其他资源尚未就绪的状态称为创建状态。无论是系统创建的进程还是用户创建的进程都是一样的,操作系统提供fork接口创建进程。

  • 终止状态

  进程终止也分为两步:首先进行系统清理,然后进行PCB归还。进程结束由系统清理或者归还PCB的状态称为终止状态。

进程同步

生产者-消费者模型

  有一群生产者进程在生产产品,并将这些产品提供给消费者进程进行消费。生产者进程和消费者进程可以并发执行,在两者之间设置了一个具有n个可缓冲区的缓冲池,生产者进程需要将所生产的产品放到其中一个缓冲区中,消费者进程可以从缓冲区中取走产品进行消费。

  生产者消费者模型在宏观上是没有问题的,当生产一个产品时缓冲区+1,当消费一个产品时缓冲区-1。但是计算机系统中是有问题的。计算机中的缓冲区位于高速缓存Cache上,生产者或消费者如果要操作缓冲区需要三个步骤:

  1. 把缓冲区中的信息取出来放入寄存器:register = count
  2. 如果生产register+1;如果消费register-1
  3. 把寄存器信息放回缓冲区里:count = register

  这三个步骤但从生产者或消费者程序看没有问题,但两者并发执行时就会出差错:

哲学家进餐问题

  有五个哲学家,他们的生活方式是交替的进行思考和进餐。哲学家们共同使用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五支筷子。平时哲学家们只进行思考,饥饿时则试图取靠近他们的左、右两支筷子,只有两支筷子都被拿到时才能进餐,进餐完毕后分别放下左、右的筷子继续思考。

  通常的情况下,假设一个哲学家饿了,首先拿起左边的筷子,如果发现右边的筷子被拿走,则需要等待右边的筷子释放,释放后才能拿起筷子吃饭。但有一种极端情况:五个哲学家同时拿起左边的筷子,此时发现他们右边的筷子都被拿走,所以五个哲学家都需要等待右边筷子释放,最终的结果就是五个哲学家被饿死。

进程同步

  上面两个问题是操作系统中的经典问题,问题的根源在于:彼此之间没有通信。如果生产者通知消费者已经完成了一件生产,或者哲学家向旁边的哲学家说我要进餐了,就可以避免这些问题。

  进程的同步用于解决这种对竞争资源在多进程间进行使用次序的协调,使得并发的多个进程之间可以有效使用资源和相互合作

进程同步原则

  临界资源指的是一些虽然作为共享资源却无法同时被多个线程共同访问的共享资源。当有进程在使用临界资源时,其他进程必须依据操作系统的同步机制等待占用进程释放该共享资源才可重新竞争使用共享资源。为了对临界资源进行约束,提出了进程同步的四个原则:

  1. 空闲让进:资源无占用时允许进程使用;
  2. 忙则等待:资源有占用,其他请求进程需要等待;
  3. 有限等待:保证有限等待时间能够使用资源;
  4. 让权等待:进程等待时,需要让出CPU;

线程同步

  和进程同步一样,一个进程内有多个线程并发使用,也会产生生产者-消费者问题和哲学家进餐问题,所以进程内的多线程也需要同步。

Linux的进程管理

进程类型

  Linux下进程分为三种类型:前台进程、后台进程、守护进程。

  • 前台进程

  Linux下有一个重要概念,就是终端Shell,我们使用命令行来使用Linux时就是通过终端Shell来实现。前台进程就是具有终端Shell,可以和用户交互的进程。前台进程占用终端Shell但不一定有输出。

  • 后台进程

  和前台进程相对,没有占用终端的就是后台进程。后台进程基本上不和用户交互,优先级比前台进程低。Linux下将需要执行的命令以“&”符号结束来启动后台进程。

  • 守护进程

  守护(daemon)进程是特殊的后台进程。很多守护进程在系统引导的时候启动,一直运行到系统关闭。Linux下进程名字以d结尾的进程一般都是守护进程,比如定时任务守护进程crond、http服务守护进程httpd、ssh登录的守护进程sshd、数据库守护进程mysqld。

进程标记

  • 进程ID

  进程的唯一标识符,每个进程拥有不同的ID。进程ID表现为一个非负整数,最大值由操作系统限定。

  操作系统提供fork接口创建进程,进程可以多层创建,这些进程是父子进程的关系。进程父子关系可以用pstree命令查看。

  需要记住几个特殊的进程:ID为0的进程为idle进程,是系统创建的第一个进程;ID为1的进程为init进程,是0号进程的子进程,完成系统的初始化。init进程也是所有用户进程的祖先进程;

  • 进程标记

  和Windows系统类似,Linux下的进程也有不同的进程状态,我们可以通过man ps命令来查看进程状态的所有标记,Linux进程状态很多,常见的有以下几种:

状态符号 状态说明
R (TASK_RUNNING),进程正处于运行状态
S (TASK_INTERRUPTIBLE),进程正处于睡眠状态
D (TASK_UNINTERRUPTIBLE),进程正处于IO等待的睡眠状态
T (TASK_STOPPED),进程正处于暂停状态
Z (TASK_DEAD or EXIT_ZOMBIE),进程正处于退出状态,或僵尸进程

Linux操作进程命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ ps命令:用于显示进程信息的常用命令,常用选项如下:
+ -e:显示所有进程,包括系统进程
+ -f:显示完整进程信息,包括进程的详细信息
+ -l:显示更多的列,包括进程的状态、CPU使用情况等
+ -u:显示指定用户的进程信息
+ -aux:结合-a和-u选项,显示所有用户的所有进程,并显示详细信息
+ -p:显示指定进程号PID的进程信息
+ -k --sort:按指定的列排序进程信息,常见的排序屁啊包括&#37;cpu(CPU使用率)、&#37;mem(内存使用率)等
+ -c:根据命令行显示进程信息
+ -o:自定义输出格式,可以指定要显示的列
+ --forest:以树状结构显示进程间的父子关系

ps命令可以和其他命令结合:
ps -aux | grep '进程名':查找指定进程的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
+ top命令:系统监视工具,查看进程状态,常用选项如下:
+ -d:秒数,指定top命令每个几秒更新
+ -b:使用批处理模式输出,一般和"-n"选项合用,用于把top命令重定向到文件中
+ -n:次数,指定top命令执行的次数,和"-"选项合用
+ -p:仅查看指定PID的进程
+ -s:使top命令在安全模式中运行,避免在交互模式中出现错误
+ -u:用户名,只监听某个用户的进程

在top命令的显示窗口中,还可以使用如下按键进行交互操作:
+ ?或h:显示交互模式的帮助
+ c:按照CPU的使用率排序,这个是默认的
+ M:按照内存使用率排序
+ N:按照PID排序
+ T:按照CPU的累积运算时间排序
+ k:按照PID给予某个进程一个信号,一般用于终止某个进程
+ r:按照PID给某个进程重设优先级
+ q:退出top命令


使用top命令后展示的进程信息关键字含义如下:
+ PID:进程标识符
+ USER:运行进程的用户名
+ PR:进程的优先级
+ NI:进程的优先级调整值
+ VIRT:进程使用的虚拟内存大小
+ RES:进程实际使用的物理内存大小
+ SHR:进程的共享内存大小
+ %CPU:进程占用CPU的使用率
+ %MEM:进程占用内存的使用率
+ TIME+:进程的累计CPU时间
+ COMMAND:进程命令行
1
2
3
4
5
6
kill命令:给进程发送信号时使用

kill -l:查看操作系统支持的所有信号
//只有SIGKILL 9信号可以无条件终止进程,其他信号进程有权忽略

eg:kill -9 PID :给指定PID进程发送9信号,用于强制退出进程

进程同步方法

  • 消息队列
  • 共享存储
  • 信号量

线程同步的方法:

  1. 互斥量
  2. 读写锁
  3. 自旋锁
  4. 条件变量

操作系统发展历程

  多道程序设计是个很重要的概念。早期的批处理系统一次只能处理一个任务,多道程序设计使得批处理系统可以一次处理多个任务,大大提升系统运行效率。多道程序设计是指在计算机内存中同时存放多个程序,程序之间相互不干扰,在计算机的管理程序之下相互穿插运行。对多道程序的管理是操作系统的重要功能。

  操作系统对多道程序和系统资源的管理,可以分为五大功能:进程管理存储管理作业管理文件管理设备管理

操作系统概述

  操作系统是管理计算机硬件和软件资源的计算机程序,本质是个软件程序。操作系统管理配置内存、决定资源供需顺序、控制输入输出设备等;操作系统也提供让用户和系统交互的操作界面。操作系统的种类是多样的,不局限于计算机,从手机到超级计算机,操作系统可简单也可复杂,在不同设备上,操作系统可向用户呈现多种操作手段。

操作系统基本功能

  计算机系统中有几大资源:处理器资源、存储器资源、IO设备资源、文件资源。操作系统第一个基本功能是统一管理计算机资源。

  操作系统的第二个功能是实现了对计算机资源的抽象,用户无需面向硬件接口编程:如IO设备管理软件提供读写接口、文件管理软件提供文件操作接口。

  操作系统的第三个基本功能是提供了用户与计算机之间的接口。有三种接口形式:图形窗口形式(比如Windows界面)、命令形式(shell终端敲入命令)、系统调用形式(程序调用的接口)。

操作系统特性

并发性

  并发性是后面三种特性的前提,只有理解了并发性,才能理解其他特性。

  并发需要和并行区分理解:并行是指两个或多个事件可以在同一时刻发生;并发是指两个或多个事件可以在同一时间间隔内发生。

  多道程序设计是并行和并发的基础。在单处理器的系统中只存在并发的概念,同一时刻只有一个程序占用CPU,多个程序交替执行;多处理器系统中,单个处理器并发执行,所有处理器并行执行。

共享性

  共享性表现为操作系统中的资源可供多个并发程序共同使用,这种共同使用的形式称之为资源共享。

  资源共享根据属性可以分为互斥共享形式和同时访问形式:

  • 互斥共享:当资源被程序A占用时,其他想使用资源的话只能等待A使用完,比如打印机资源;
  • 同时访问:某种资源在一段时间内并发的被多个程序访问,这种“同时”是宏观的,从宏观看资源可以被同时访问,比如硬盘资源;

虚拟性

  虚拟性表现为把一个物理实体转变为若干个逻辑实体,物理实体是真实存在的,逻辑实体是虚拟的。虚拟技术主要有时分复用技术空分复用技术

  • 时分复用:资源在时间上进行复用,不同程序并发使用,多道程序分时使用计算机的硬件资源,时分复用可以提高资源利用率;
  • 空分复用:用来实现虚拟磁盘、虚拟内存等,提高资源的利用率,提升编程效率;

异步性

  异步性表现为在多道程序的环境下,允许多个进程并发执行。由于进程在使用资源时可能需要等待或放弃,所以进程的执行并不是一气呵成的,而是以走走停停的形式推进。

创建分支

1
$ git branch 分支名

删除分支

1
2
3
4
5
# -d必须要求该分支已经被合并
$ git branch -d 分支名

# -D强制删除
$ git branch -D 分支名

修改commit的message

1
2
3
# 修改当前branch最新的commit的message:
$ git commit --amend
# 命令输入后可以修改message,linux命令行wq!退出编辑
1
2
3
$ git rebase -i 需要变更的commit的上一次commit的hash

# 命令输入后进入交互界面,根据界面中的指令选择对应的命令,此处应该用r

把多个commit整理为一个

连续的commit整理为一个:

1
2
3
4
$ git rebase -i 需要整理的最早的commit的hash

# 命令输入后进入交互界面,根据界面中的指令选择对应的命令
# 多个分支选择一个用pick命令,其余用squash指令

比较暂存区和HEAD所含文件

  省事的做法是,修改完文件后直接把暂存区文件commit提交,这是个不好的习惯。一个比较好的做法是,在执行commit之前,需要先比较一下当前暂存区的文件是否合适提交到当前分支。

1
2
# 比较暂存区和HEAD
$ git diff --cached

比较工作区和暂存区

1
2
3
$ git diff 

$ git diff --文件名

暂存区恢复为HEAD

1
2
3
4
5
# 恢复所有暂存区文件
$ git reset HEAD

# 恢复暂存区部分文件
$ git reset HEAD -- 文件名

工作区恢复为暂存区

1
$ git checkout --文件名

commit回退

这个命令直接操作了head指针,把工作区和暂存区的东西都修改了,轻易不要使用。

1
$ git reset --hard 指定的commit的hash

比较不同branch之间的差异

1
2
3
4
5
$ git diff 分支1 分支2

$ git diff 分支1 分支2 -- 具体文件名

$ git diff hash1 hash2

删除文件

1
2
# 暂存区里删除文件
$ git rm 文件名

暂存修改内容

一般用于开发过程中出现紧急任务,当前分支需要做修改。

1
2
3
4
5
6
7
8
9
10
11
12
# 暂存当前工作区内容
$ git stash

# 查看所有暂存内容
$ git stash list


# 暂存区内容恢复,但不清空stash堆栈
$ git stash apply

# 暂存区内容恢复,且清空stash堆栈
$ git stash pop

指定不需要git管理的文件

对于一些代码生成过程中产生的构建文件,是不需要参与git管理的,每次生成都可以重新产生。

github中有一个文件".gitignore"可以指定,这个文件由github自动生成,不同语言的内容不一样。

1
2
3
*.d     //所有.d后缀的文件都不纳入管理

*.dsYM/ //dsYM文件夹下的任何文件都不纳入git管理,但如果有文件是.dsYM后缀仍然需要掌控

git备份

常用协议 语法格式 说明
本地协议 /path/to/repo.git 哑协议
本地协议 file://path/to/repo.git 智能协议
http/https协议 http://git-server.com:port/path/to/repo.git
https://git-server.com:port/path/to/repo.git
平时经常接触的智能协议
ssh协议 user@git-server.com:path/to/repo/git 工作中常用的智能协议

哑协议和智能协议的区别:

  1. 哑协议传输进度不可见、智能协议传输可见;
  2. 智能协议传输速度更快;
1
2
3
$ git clone 协议地址 本地路径  //远程仓库拉取到本地

$ git remote add <file> <远端地址>

汇编语言

  用汇编语言翻译机器语言,对人类更友好。要理解汇编语言,需要知道寄存器、基本指令、中断等基础知识。由于汇编语言偏向底层,所以不同的系统上汇编语言是不一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用汇编语言实现对话框弹出helloworld


extern MessageBoxA // 引入MessageBox

//代码段
section .text
golbal main

main:
push dword 0
push dword title
push dword text
push dword 0 // 传递四个参数
call MessageBoxA //调用MessageBoxA
ret //返回



//数据段
section .data
text: db 'Hello World',0
title: db 'MessageBox',0

常见寄存器

  • 通用寄存器:
    • 数据寄存器:EAX、EBX、ECX、EDX
    • 变址寄存器:ESI、EDI
    • 指针寄存器:ESP、EBP
  • 段寄存器:CS、DS、ES、SS、GS
  • 指令寄存器:EIP
  • 标志寄存器:CF、PF、AF、ZF、SF、OF

常见基本语句

  • 基本运算:INC(加法)、ADD(加法)、DEC(减法)、SUB(减法)、mul(乘法)、div(除法)
  • 循环、跳转:loop(循环)、call(函数调用)、jmp(跳转)、ret(返回)
  • 数据移动:mov、lea
  • 栈操作:PUSH、POP
  • 逻辑运算:AND、OR、XOR(异或)、NOT
  • 比较运算:cmp、test

中断

  汇编语言需要和底层打交道,这里就需要有中断机制

  • 内中断:和CPU直接交互
  • int指令
  • 端口:对应硬件设备的交互
  • 外中断:来自外设的中断

  

从汇编看函数栈

  在编程语言中函数是以栈结构来设计的,先调用的函数后返回。

1
2
3
4
5
6
7
8
9
10
11
12
void firstCall(int x, int y)
{
int z = x + y;
int a = 1;
}

int main(int argc, char* argv[])
{
firstCall(1, 2);
return 0;
}

栈是由高地址向低地址生长的,所以每次push操作都会使ESP的值减小,相反每次POP操作都会使ESP增大;

Linux开发环境搭建

gcc、g++的安装

gcc和g++是GUN的C&C++编译器,这两个本质上区别不大,gcc默认下使用C编译器,g++默认使用C++编译器。

1
2
3
yum install gcc

yum install gcc-c++

编译流程

  • 源文件生存可执行程序:
1
2
3
4
5
6
7
8
>$ g++ helloworld.cpp -o helloworld


// 如果用gcc编译器,不会包含标准库信息,需要在命令行中包含
>$ gcc -lstdc++ helloworld.cpp -o helloworld

//-wall可以看到警告信息
>$ g++ -Wall helloworld.cpp -o helloworld

GCC命令的编译选项:

参数 解释
-ansi 只支持ANSI标准的C语法。这一选项将进制GNU C的某些特色,例如asm或者typeof关键字
-S 只激活预处理和编译,就是指把文件编译成为汇编代码
-c 只编译并生成目标文件
-g 生成调试信息。GNU调试器可利用该信息
-o FILENAME 生成指定的输出文件。用在生成可执行文件时
-O0 不进行优化处理
-O或-O1 优化生成代码
-O2或-O3 进一步优化
-shared 生成共享目标文件。通常用在建立共享库时
-static 禁止使用共享连接
-w 不生成任何警告信息
-Wall 生成所有警告信息
-IDIRECTORY 指定额外的头文件搜索路径DIRECTORY
-LDIRECTORY 指定额外的函数库搜索路径DIRECTORY
-ILIBRARY 链接时搜索指定的函数库LIBRARY
-m486 针对486进行代码优化
-E 只运行C预编译器

make和Makefile

  make是一个批处理工具,本身其实没什么功能。make工具就根据Makefile中的命令进行编译和链接。其实Windows系统中也有这些步骤,只不过微软已经把这些嵌入到了编译器中,不需要程序员去关心。Linux系统也有一些IDE可以帮助我们完成这些工作,比如CMake。

makefile的作用

工程中可执行文件的产生过程如下:

  1. 配置环境(系统环境)
  2. 确定标准库和头文件的位置
  3. 确定依赖关系(源代码之间编译的依赖关系)
  4. 头文件预编译
  5. 预处理
  6. 编译
  7. 链接
  8. 安装
  9. 和操作系统建立联系
  10. 生成安装包

  大型工程中需要确定代码之间的依赖关系(第三步),当依赖关系复杂的时候,make命令工具诞生了,而Makefile文件正是为make工具所使用的。Makefile描述了整个工程文件的编译顺序、编译规则。

make流程

  假设我们有一个简单的demo,reply.h、reply.cpp两个文件定义了一个类,输出“helloworld”,main.cpp是主函数文件,生成类对象。这三个文件有依赖关系。我们编译的步骤如下:

  1. 写Makefile文件
1
2
3
4
5
6
main: reply.o main.o    //左侧main是目标,依赖右侧的两个文件
g++ reply.o main.o -o main //使用g++命令,用reply.o、main.o生成main
reply.o: reply.cpp //reply.o,依赖reply.cpp
g++ -c reply.cpp -o reply.o //用g++命令生成reply.o,只编译
main.o: main.cpp //main.o,依赖main.cpp
g++ -c main.cpp -o main.o //用g++命令生成main.o,只编译

  可以看到,先表明最终生成文件的依赖关系,然后生成。其次挨个写被依赖文件自身的依赖关系。从上往下是倒置的依赖关系。

  1. 用make命令

$ make

  类似于批处理,make命令会去调用makefile文件,完成Makefile文件中的各项命令。

makefile文件格式

  • makefile的基本规则:
1
2
目标(target)...: 依赖(prerequisites)
命令(command)

注意:每个命令前必须是Tab字符。

  • makefile的简化规则:

    1. 变量定义: 变量 = 字符串
    2. 变量使用: $(变量名)
1
2
3
4
5
6
7
8
9
10
11
//上面的makefile文件可以做如下简化:

TARGET = main
OBJS = reply.o main.o
$(TARGET): $(OBJS)
g++ $(OBJS) -o $(TARGET)
reply.o: reply.cpp
main.o: main.cpp

clean:
rm $(TARGET) $(OBJS) //生成完成后

  • 清空操作
1
2
3
4
5
6
7
8
9
//在makefile文件中可以加上清理操作:

TARGET = main
OBJS = reply.o main.o

.PHONY: clean //.PHONY关键字,表示clean不存在,否则目录下存在"clean"同名文件的话,clean会失败

clean:
rm $(TARGET) $(OBJS)

执行$ make clean命令后,可以根据makefile中定义的删除操作把文件删掉。

makefile的扩展用法

  1. make工程的安装和卸载
1
2
3
4
5
6
7
8
9
10
TARGET = main
OBJS = reply.o main.o

//安装可以简化为拷贝操作
//把main拷贝到/user/local/bin/mainTest
install:
cp ./main /user/local/bin/mainTest

uninstall:
rm /user/local/bin/mainTest

  1. Makefile中的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
一、用户自定义变量
变量 = 字符串,使用的时候就用$()括起来。就类似于C++中的宏定义。注意变量的大小写敏感。



二、变量中的变量
变量可以先使用再声明:
foo = $(bar) //还没声明bar,但可以先直接用
bar = $(ugh)
ugh = Hug

这样可以使变量定义更加灵活,但如果工程过于复杂不建议用,因为这样定义需要make来推导
定义的层级过多,会导致编译速度很慢。

如果希望只能使用声明过的变量,那么可以使用":="来替换"="
":="如果使用了还没有声明的变量,会失效



三、追加变量
可以使用"+="来追加变量



四、多行变量
define two-lines
第一行命令
第二行命令
endif



五、环境变量
就是操作系统的环境变量,Windows和Linux下都有


六、自动变量(目标变量)
上面说的变量都是全局变量,整个makefile文件的运行过程中都可以被访问。
自动变量和上面的不一样,这是一种规则变量,设定好规则后,只有makefile运行过程中符合规则时才生效
$< : 表示第一个匹配的依赖
$@ : 表示目标
$^ : 表示所有依赖
$? : 表示所有依赖中更新的文件
$+ : 表示所有依赖文件不去重
$(@D) : 表示目标文件路径
$(@F) : 表示目标文件名称


七、模式变量
模式变量就是符号'%',比较类似在搜索时使用的通配符,可以表示任何一个字符


八、自动匹配

Makefile自动生成部署——CMake

  一般来说,项目的目录结构来说起码要包含如下目录:src目录下是头文件的实现文件;include目录下是包含的头文件;bin目录下是可运行文件;build目录下是临时构建的文件。当我们把文件分门别类后,可以让整个项目的层次分明。但对于大型工程来说,这么复杂的目录结构会导致makefile的编写极其困难。

  为了解决这两个问题,有几个非常好用的工具:automake/autocinfigCMake

  生成CMake工程不需要写makefile文件,又CMake自动生成,我们只需要写CMakeList.txt这样的文本文件即可,这个文件是有模板的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# CMakeList.txt
# 设置cmake最低版本
cmake_minimum_required(VERSION 2.8.0)
# 设置C++标准
set(CMAKE_CXX_STANDARD 11)
# 项目名称
project(cmake_test)
# 包含的头文件目录
include_directories(./include)
set(SRC_DIR ./src)
# 指定生成链接库
add_library(XXX ${SRC_DIR}/XXX.cpp)
add_library(YYY ${SRC_DIR}/YYY.cpp)
# 设置变量
set(LIBRARIES XXX YYY)
set(OBJECT XXX_test)
# 生成可执行文件
add_executable(${OBJECT} ${SRC_DIR}/main.cpp)
# 为可执行文件链接目标库
target_link_libraries(${OBJECT} ${LIBRARIES})

  早期的C++语言并不支持多线程,认为会影响到语言的性能。早期的C++程序多线程的实现是交给操作系统内生API来实现。在C++11之后把Thread类纳入了C++体系,C++可以支持多线程开发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;

mutex g_mutex;
void T1()
{
g_mutex.lock();
cout << "T1 Hello" << endl;
g_mutex.unlock();
}
void T2(const char* str)
{
g_mutex.lock();
cout << "T2 " << str << endl;
g_mutex.unlock();
}
int main()
{
thread t1(T1); //传递线程的入口函数
thread t2(T2, "Hello World"); //还可以跟函数的参数
t1.join();
//t2.join();
t2.detach();
cout << "Main Hi" << endl;


return 0;
}


  • thread::join:主线程等待子线程执行结束
  • thread::detach:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//例子:银行存取钱

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;


// 存钱
void Deposit(mutex& m, int& money)
{
// 锁的粒度尽可能的最小化,只锁要保护的变量,别放循坏外面把整个流程都锁了
for(int index = 0; index < 100; index++)
{
m.lock();
money += 1;
m.unlock();
}
}
// 取钱
void Withdraw(mutex& m, int& money)
{
// 锁的粒度尽可能的最小化
for (int index = 0; index < 100; index++)
{
m.lock();
money -= 2;
m.unlock();
}
}

int main()
{
// 银行存取款
int money = 2000;
mutex m; //这个锁尽量不要搞全局的,不好控制
cout << "Current money is: " << money << endl;
thread t1(Deposit, ref(m), ref(money)); //引用的传递:ref()
thread t2(Withdraw, ref(m), ref(money));
t1.join();
t2.join();
cout << "Finally money is: " << money << endl;




//线程交换
thread tW1([]()
{
cout << "ThreadSwap1 " << endl;
});
thread tW2([]()
{
cout << "ThreadSwap2 " << endl;
});
cout << "ThreadSwap1' id is " << tW1.get_id() << endl;
cout << "ThreadSwap2' id is " << tW2.get_id() << endl;

cout << "Swap after:" << endl;
swap(tW1, tW2); //交换线程的句柄
cout << "ThreadSwap1' id is " << tW1.get_id() << endl;
cout << "ThreadSwap2' id is " << tW2.get_id() << endl;
tW1.join();
tW2.join();




// 线程移动
thread tM1( []() { ; } );
//tM1.join();
cout << "ThreadMove1' id is " << tM1.get_id() << endl;
cout << "Move after:" << endl;
thread tM2 = move(tM1); //转移所有权
cout << "ThreadMove2' id is " << tM2.get_id() << endl;
cout << "ThreadMove1' id is " << tM1.get_id() << endl;
tM2.join();

return 0;
}

STL标准模板库

  STL全称Standard Template Library,标准模板库。STL算法是泛型的(generic),不与任何特定数据结构和对象绑定,不必再环境类似的情况下重写代码。STL算法可以量身定做,并且具有很高的效率。STL可以进行扩充,我们可以编写自己的组件并且能够与STL标准的组件进行很好的配合。

  STL标准库有六大组件:

  • 空间配置器:allocator;
  • 容器:string、vector、list、deque、map、set、multimap、multiset;
  • 适配器:stack、queue、priority_queue;
  • 仿函数:greater、less等;
  • 算法:find、swap、reverse、sort、merge等;
  • 迭代器:iterator、const_iterator、reverse_iterator、const_reverse_iterator;

  空间配置器帮助我们管理分配内存的事情,从而来给容器分配内存;容器存储数据;迭代器帮助算法处理容器中的数据;仿函数辅助算法实现。

  

容器(container)

  STL容器的作用用于存放数据,除了引用类型外,所有内置或复合类型都可用作容器的元素类型。引用不支持一般意义的赋值运算,因此没有元素是引用类型的容器。

  在容器中添加元素时,系统是将元素值复制到容器里。类似的,使用一段元素初始化新容器时,新容器存放的是原始元素的副本。被复制的原始值与新容器中的元素各不相关,此后,新容器内元素值发生变化时,被复制的原值不会受到影响,反之亦然。

STL容器分为两大类:

  • 序列式容器:其中的元素都是可排序的,STL提供了vector、list、deque等序列式容器,而stack、queue、priority_queue则是容器适配器;
  • 关联式容器:每个数据元素都是由一个键(key)和值(value)组成,当元素被插入到容器时,按照其键以某种特定规则放入适当位置,key必须是唯一的;常见的STL关联容器有set、multiset、map、multimap;

序列型容器的基本使用

  1. vector

  vector的数据安排以及操作方式类似数组。两者唯一的差别在于空间运用的灵活性。数组是静态空间,一旦配置了就不能改变;vector是动态空间,随着元素的加入,它的内部机制可以动态增加或减少元素,内存管理自动完成,但程序员可以使用reverse()来管理内存。

  vector类提供了两个成员函数:capacity和reverse,使程序员可与vector容器内存分配的实现部分交互工作。capacity操作获取在容器需要分配更多的存储空间之前能够存储的元素总数,而reverse操作则告诉vector容器应该预留多少个元素的存储空间。

  capacity和size的区别:size指容器当前拥有的元素个数;capacity指容器必须在分配新存储空间之前可以存储的元素总数。插入元素时,若size < capacity,则直接插入数组,当元素个数超过capacity()时,因为旧空间不足而需要重新分配一块更大空间,通常多数实现是将capacity增加一倍(capacity为0时,增加1);然后重新分配一块大小等于新capacity的内存;接着复制旧元素到到新内存中,最后释放旧内存,并追加新的数据。

  push_back操作接受一个元素值,并将它作为一个新的元素添加到vector对象的后面,也就是“插入”到vector对象“后面”。vector在末尾增加或删除元素所需时间与元素数目无关,在中间或开头增加或删除元素所需时间隋元素数目呈线性变化。

  vector的迭代器在内存重新分配时将失效(它所指向的元素在该操作的前后不再相同)。插入元素后,指向当前插入元素之后的任何元素的迭代器都将失效。当插入元素后,元素个数超过capacity()时,内存会重新分配,此时所有的迭代器都将失效。当删除元素时,指向被删除元素之后的任何元素的迭代器都将失效。

  clear()方法可以清空内部数据,但需要注意此时仅仅是size()=0,实际的内存并没有释放掉。如果需要强行释放,可与用swap的方法,交换迭代器的地址,即:"v.swap(vector())"直接释放v的内存。

  1. list

  list的内部数据结构是一个双向环状链表,故不能随机访问一个元素(没有find操作),但可以双向遍历,可动态增加或减少元素,内存管理自动完成,增加任何元素都不会使迭代器失效。删除元素时,除了指向当前被删除元素的迭代器外,其他迭代器都不会失效。

  1. deque

  vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间,可以在头尾两端分别做元素的插入和删除操作。vector从技术上也可以双端操作,但头部操作的效率奇差,无法被接受。

  deque和vector的另一个差异是deque没有容量(capacity)的概念,因为它是动态地以分段连续空间组合而成的,随时可以增加一段新的空间并链接起来。

  deque的特点是:1)随机访问每个元素,所需要的时间为常量;2)在开头和末尾增加元素所需要的时间与元素数目无关,在中间增加或删除元素所需要的时间随元素数目呈线性变化;3)可动态增加或减少元素,内存管理自动完成,不提供用于内存管理的成员函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <vector>
#include <list>
#include <queue>
#include <stack>
#include <map>
#include <string>
#include <functional>
#include<algorithm>
#include <utility>
#include <iostream>
using namespace std;

//结构体中重载了函数调用运算符operator(),可以让其像函数一样被调用
//常用于STL算法中的回调
//这样写可以让函数以一个类对象的形式被调用
struct Display
{
void operator()(int i)
{
cout << i << " ";
}
};

int main()
{
int iArr[] = { 1, 2,3,4,5 };

vector<int> iVector(iArr, iArr + 4); //传递首地址和尾地址,用一个数组初始化vector
list<int> iList(iArr, iArr + 4);
deque<int> iDeque(iArr, iArr + 4);

//下面三个是容器适配器,不是容器
queue<int> iQueue(iDeque); // 队列 先进先出
stack<int> iStack(iDeque); // 栈 先进后出
priority_queue<int> iPQueue(iArr, iArr + 4); // 优先队列,按优先权,默认从大到小

//序列型容器的遍历
//for_each:遍历,传递首尾和一个函数,对于遍历的每一个元素都调用一次函数
for_each( iVector.begin(), iVector.end(), Display() );
cout << endl;
for_each(iList.begin(), iList.end(), Display());
cout << endl;
for_each(iDeque.begin(), iDeque.end(), Display());
cout << endl;


//容器适配器的遍历:
while ( !iQueue.empty() )
{
cout << iQueue.front() << " "; //输出队首 1 2 3 4
iQueue.pop(); //弹出队首
}
cout << endl;

while (!iStack.empty())
{
cout << iStack.top() << " "; // 输出栈顶 4 3 2 1
iStack.pop(); //弹出栈顶
}
cout << endl;

while (!iPQueue.empty())
{
cout << iPQueue.top() << " "; // 不设置的话默认从大到小4 3 2 1
iPQueue.pop();
}
cout << endl;



return 0;
}

关联式容器的基本使用

  关联容器和序列容器的本质差别在于:关联容器通过键(key)存储和读取元素,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。虽然关键容器的大部分行为与顺序容器相同,但其独特之处在于支持键的使用。

  所有关联式容器的底层都是通过红黑树实现的。

  1. map

  map的所有元素的键值都会被自动排序。map的所有元素都是pair,同时拥有实值和键值。pair的第一元素被视为键值,第二元素被视为实值。map不允许两个元素拥有相同的键值。

  map拥有跟list相同的某些性质:当客户端对它进行元素新增操作或删除操作时,除了被删除的那个元素的迭代器之外,操作之前的所有迭代器,在操作之后依然有效。

  multimap的特性以及用法和map完全相同,唯一的差别在于它允许键值重复。

  1. set

  set的所有元素都会根据元素的键值被自动排序,set的元素不像map那样可以同时拥有实值和键值,set元素的键值就是实值。set不允许两个元素有相同的键值。

  set拥有跟list相同的某些性质:当客户端对它进行元素新增操作或删除操作时,除了被删除的那个元素的迭代器之外,操作之前的所有迭代器,在操作之后依然有效。

  multiset的特性以及用法和set完全相同,唯一的差别在于它允许键值重复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
struct Display
{
void operator()(pair<string, double> info)
{
cout << info.first << ": " << info.second << endl;
}
};

struct cmpMap
{
bool operator()(pair<string, double> a, pair<string, double> b)
{
return a.first.length() < b.first.length();
}
};


int main()
{
map<string, double> studentSocres;

//map可以用类似数组的方式来添加
studentSocres["LiMing"] = 95.0;
studentSocres["LiHong"] = 98.5;

//map也可以用pair来添加
studentSocres.insert( pair<string, double>("zhangsan", 100.0) );
studentSocres.insert(pair<string, double>("Lisi", 98.6));
studentSocres.insert(pair<string, double>("wangwu", 94.5));
studentSocres.insert(pair<string, double>("wangwu", 96.5)); //这是失败的操作,因为key值重复了

//map还可以用value_type来添加,是泛型编程的方式
studentSocres.insert(map<string, double>::value_type("zhaoliu", 95.5) );
studentSocres["wangwu"] = 88.5; //想修改已经存在的key值的操作,要用这个方式



// map用for_each遍历
for_each(studentSocres.begin(), studentSocres.end(), Display());
cout << endl;

// map用find查找,查找结果会返回一个迭代器
map<string, double>::iterator iter;
iter = studentSocres.find("zhaoliu");
if (iter != studentSocres.end())
{
cout << "Found the score is: " << iter->second << endl;
}
else
{
cout << "Didn't find the key." << endl;
}

// 使用迭代器完成遍历查找的过程
iter = studentSocres.begin();
while (iter != studentSocres.end())
{
if (iter->second < 98.0) // 去除不是优秀的同学
{
studentSocres.erase(iter++); // 注意:迭代器失效问题
// erase后,当前传递的迭代器参数就失效了,所以必须要++指向后一个元素
}
else
{
iter++;
}
}
for_each(studentSocres.begin(), studentSocres.end(), Display());
cout << endl;


for (iter = studentSocres.begin(); iter != studentSocres.end(); iter++)
{
if (iter->second <= 98.5)
{
iter = studentSocres.erase(iter); // 注意:迭代器失效问题
// erase的返回值指向迭代器的下一个元素,如果不想在参数里做iter++,就把返回值获取下
}
}
for_each(studentSocres.begin(), studentSocres.end(), Display());
cout << endl;

// find得到并删除元素
iter = studentSocres.find("LiHong");
studentSocres.erase(iter); //这里最好做个判空操作
for_each(studentSocres.begin(), studentSocres.end(), Display());

int n = studentSocres.erase("LiHong1"); //返回的是去除元素的计数值,不为零就是成功
cout << n << endl;
for_each(studentSocres.begin(), studentSocres.end(), Display());


// 一次性清空所有元素
studentSocres.erase(studentSocres.begin(), studentSocres.end());
for_each(studentSocres.begin(), studentSocres.end(), Display());
cout << endl;

return 0;
}

容器的选用

  list容器表示不连续的内存区域,允许向前和向后逐个遍历元素。在任何位置都可高效地insert或eraser一个元素。插入或删除list容器中的一个元素不需要移动任何其他元素。另一方面,list不支持随机访问,访问某个元素要求遍历涉及的其他元素。

  对于vector容器来说,除了容器尾部外,其他任何位置的插入和删除操作都要求移动被插入或删除的元素右边的所有元素。

  deque容器有更加复杂的数据结构。从deque队列的两端插入和删除元素都非常快。在容器中间插入或删除比较慢。deque容器支持堆所有元素的随机访问。

  综上所述,容器选择的法则为:

  1. 如果程序要求随机访问元素,则应使用vector或deque;
  2. 如果程序必须在容器的中间位置插入或删除元素,则应采用list;
  3. 如果程序需要在首部或尾部插入或删除元素,使用deque;
  4. 如果只需要在读取输入时在容器的中间位置插入元素,然后需要随机访问元素,则可以在输入时将元素读入到一个list容器,接着对list容器重新排序,使其适合顺序访问,然后将排序后的list容器复制到一个vector中;
  5. 如果既需要随机访问又需要在容器中间插入或删除数据,那么需要考虑那种操作的代价更大,主要看哪种操作的概率更大;
  6. 如果实在不知道选取哪个容器,则编写代码时应尝试只使用vector和list容器提供的操作:使用迭代器而不是下标访问,并且避免随机访问元素。这样编写后,在必要时可以很方便的将程序从使用vector容器修改为使用list容器;

仿函数(functor)

  仿函数一般不会单独使用,主要是为了搭配STL算法。STL主要是为了提高代码的复用性,早期C++为了提高复用性一般使用函数指针,但是函数指针不能满足STL对抽象性的要求,不能满足软件积木的要求,无法和其他STL组件搭配。仿函数的本质就是一个重载了operator()的类,创建一个行为类似函数的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 不同方式实现对数组元素的排序
// sort是C++中的一个排序算法


#include <algorithm>
#include <iostream>
using namespace std;

//C++函数
bool MySort(int a, int b)
{
return a < b;
}
void Display(int a)
{
cout << a << " ";
}

// 泛型编程
// 定义为const&是为了优化,如果将来传递进来的是个对象,只需要传引用即可
template<class T>
inline bool MySortT(T const& a, T const& b)
{
return a < b;
}
template<class T>
inline void DisplayT(T const& a)
{
cout << a << " ";
}

// 仿函数
struct SortF
{
bool operator() (int a, int b)
{
return a < b;
}
};
struct DisplayF
{
void operator() (int a)
{
cout << a << " ";
}
};

// C++仿函数模板
template<class T>
struct SortTF
{
inline bool operator() (T const& a, T const& b) const
{
return a < b;
}
};
template<class T>
struct DisplayTF
{
inline void operator() (T const& a) const
{
cout << a << " ";
}
};


int main()
{
// C++方式
// 缺点是不通用,不同的数据类型要写不同的MySort函数
int arr[] = { 4, 3, 2, 1, 7 };
sort(arr, arr + 5, MySort); //传递排序函数
for_each(arr, arr + 5, Display);
cout << endl;

// C++泛型
int arr2[] = { 4, 3, 2, 1, 7 };
sort(arr2, arr2 + 5, MySortT<int>);
for_each(arr2, arr2 + 5, DisplayT<int>);
cout << endl;

// C++仿函数
int arr3[] = { 4, 3, 2, 1, 7 };
sort(arr3, arr3 + 5, SortTF<int>() );
for_each(arr3, arr3 + 5, DisplayTF<int>());
cout << endl;

// C++仿函数模板
int arr4[] = { 4, 3, 2, 1, 7 };
sort(arr4, arr4 + 5, SortF());
for_each(arr4, arr4 + 5, DisplayF());
cout << endl;

return 0;
}

算法(algorithm)

STL中算法大致分为4类,包含

  1. 非可变序列算法:不直接修改其操作的容器内容的算法;
  2. 可变序列算法:可以修改它们操作容器内容的算法;
  3. 排序算法:包括对序列进行排序和合并的算法、搜索算法以及有序序列上的集合算法;
  4. 数值算法:对容器内容进行数值计算;

  最常见的算法包括查找、排序和通用算法,排列组合算法、数值算法、集合算法等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <vector>
#include <algorithm>
#include <functional>
#include <numeric>
#include<iostream>
using namespace std;


int main()
{
// transform和lambda表达式
// transform是数值算法函数,有多个重载
// 主要参数就是传递入容器的范围和范围内元素需要执行的函数。
int ones[] = { 1, 2, 3, 4, 5 };
int twos[] = { 10, 20, 30, 40, 50 };
int results[5];

//参数:第一个容器起始、第一个容器结尾,第二个容器(范围不能小于第一个容器)
//resulte用于接收结果,要保证空间足够
//std::plus是STL中的模板类仿函数方法,
transform(ones, ones + 5, twos, results, std::plus<int>());
for_each(results, results + 5,
[ ](int a)->void {
cout << a << endl; } ); // lambda表达式(匿名函数)
cout << endl;




// find 查找算法
int arr[] = { 0, 1, 2, 3, 3, 4, 4, 5, 6, 6, 7, 7, 7, 8 };
int len = sizeof(arr) / sizeof(arr[0]);


cout << count(arr, arr + len, 6) << endl; // 统计“6”这个元素的个数,输入容器范围和元素
cout << count_if(arr, arr + len, bind2nd(less<int>(), 7) ) << endl; // 统计<7的个数
/*
bind2nd是模板方法,输入一个函数和一个变量,函数调用这个变量
bind2nd和bind1st是对应的方法,bind1st的入参是运算左边的数,bind2nd的入参是运算右边的数
比如bind2nd(less<int>(), 7),7是右边的数,意思是“?<7”,有几个比7小的数
bind1st(less<int>(), 7),7是左边的数,意思是“7<?”,7比几个数小,也就是有几个数比7大
当然也可以把less<int>()换成greater<int>(),一个是小,一个是大
*/
cout << binary_search(arr, arr + len, 9) << endl; // 二分查找,找元素"9",返回是否找到

vector<int> iA(arr + 2, arr + 6); // {2,3,3,4},创建一个子序列
cout << *search(arr, arr + len, iA.begin(), iA.end()) << endl; // 查找子序列,返回的是地址位置

cout << endl;

return 0;
}

迭代器

  迭代器是一种智能指针,用于访问顺序容器和关联容器中的元素,相当于容器和操纵容器的算法之间的中介。迭代器可以分为四种:

  1. 正向迭代器:iterator
  2. 常量正向迭代器:const_iterator,常量指向的是元素,表示迭代器指向的元素不允许修改
  3. 反向迭代器:reverse_iterator
  4. 常量反向迭代器:const_reverse_iterator

容器 迭代器功能
vector 随机访问
deque 随机访问
list 双向访问
set 双向访问
map 双向访问
stack 不支持迭代器
queue 不支持迭代器
priority_queue 不支持迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <list>
#include <iostream>
using namespace std;

int main()
{
list<int> v;
v.push_back(3); //尾插入
v.push_back(4);
v.push_front(2); //头插入
v.push_front(1); // 1, 2, 3, 4

list<int>::const_iterator it;
for (it = v.begin(); it != v.end(); it++)
{
//*it += 1; //常量迭代器,不允许我们修改指向的元素,这个违法操作
cout << *it << " ";
}
cout << endl;

// 注意:迭代器不支持<操作
//for (it = v.begin(); it < v.end(); it++)
//{
// cout << *it << " ";
//}
cout << v.front() << endl;
v.pop_front(); // 从顶部去除

list<int>::reverse_iterator it2;
for (it2 = v.rbegin(); it2 != v.rend(); it2++)
{
*it2 += 1; //非常量迭代器,允许修改元素
cout << *it2 << " "; // 5 4 3
}
cout << endl;

return 0;
}

容器适配器(adapter)

STL有三个容器适配器,注意不要和容器搞混:

  • stack 堆栈:一种“先进后出”的容器适配器,不支持遍历,底层数据结构使用的是deque;
  • queue 队列:一种“先进先出”的容器适配器,不支持遍历,底层数据结构使用的是deque;
  • priority_queue 优先队列:一种特殊的队列,它能够在队列中进行排序(堆排序),底层实现结构是vector或者deque,不同编译器实现不同,只能访问第一个元素,不能遍历整个队列;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <functional>
#include <stack>
#include <queue>
#include <iostream>
using namespace std;

int main()
{
//stack<int> s;
//queue<int> q;


priority_queue<int> pq; // 默认是最大值优先
priority_queue<int, vector<int>, less<int> > pq2; // 最大值优先
priority_queue<int, vector<int>, greater<int> > pq3; // 最小值优先

pq.push(2);
pq.push(1);
pq.push(3);
pq.push(0);
while (!pq.empty())
{
int top = pq.top(); //获取队列头部信息
cout << " top is: " << top<< endl;
pq.pop(); //弹出队列
}
cout << endl;


pq2.push(2);
pq2.push(1);
pq2.push(3);
pq2.push(0);
while (!pq2.empty())
{
int top = pq2.top();
cout << " top is: " << top << endl;
pq2.pop();
}
cout << endl;


pq3.push(2);
pq3.push(1);
pq3.push(3);
pq3.push(0);
while (!pq3.empty())
{
int top = pq3.top();
cout << " top is: " << top << endl;
pq3.pop();
}
cout << endl;

return 0;
}

空间配置器(allocator)

  从使用的角度看,空间配置器隐藏在STL组件当中,为容器默默的工作,不需要使用者关心,但是从理解STL实现的角度看,它是需要首先分析的组件。allocator的分析可以体现C++在性能和资源管理上的优化思想。allocator被叫做空间配置器而不是内存配置器,是因为它不仅仅可以在内存上分配空间,甚至可以直接在硬盘上分配一块空间。

C语言的类型转换

赋值转换

  将一种类型的值赋给另一种类型的变量,此时值会转变为接收变量的类型

1
2
3
4
5
int ival = 3.14;  //3.14被截断为3


int* ip;
ip = 0; //0被转换为int*型的空指针

  当把一个超出其范围的值赋给一个指定类型的对象时,不同的系统有不同的操作。将int类型的数赋值给short类型时,大多数系统将int的低字节赋值,高字节舍去:

1
short a = 0x1111FFFF; //a的值为0xFFFF

  当把一个取值范围小的值赋给取值范围大的值,会根据符号位自动补值:

1
2
char a = 0xe0;
int b = a; //符号位是1,补1,此时b=0xffffffe0

表达式转换

  当同一个表达式中出现不同类型的量时,会根据不同的情况对操作数进行自动转换,这些转换可分为“整数提升”和“运算时的转换”:

  1. 整数提升:表达式计算时,bool、char、unsigned char、short都会自动转换成int型,bool值类型中true转为1,false转为0;
  2. 运算时转换:运算涉及两种类型时,较小的类型(表达力低)会转换为较大的类型(表达力高),表达力从低到高的排序为“int➡unsigned int➡long➡unsigned long➡fload➡double➡long double”;

  表达式转换时long和unsigned int的转换需要注意,32位机器上long和int通常都是一个字长,所以表达式中包含unsigned int和long两种类型时,这俩统一转换为unsigend long;

  表达式中出现同类型的signed和unsigned数时,统一转换为unsigned,只是把符号位当作数值位计算。比如int型的-1会转换为unsigned int的\(2^{32}-1\),这就是常说的溢出了。要注意此时内存中的内容并没有改变,只是解释不一样。

显式转换

  显示转换也称为强制类型转换,格式为“(类型说明符)(表达式)”,比如double f = double(1)/double(2);。基本类型的指针之间不含隐式转换(void*除外),都需要显示转换。

隐式转换

  把一个表达式传递给一个函数调用、从函数中返回一个表达式会发生隐式类型转换.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
double f = 1/2;
//虽然用double变量来接收结果,但表达式运算全是整型数,会转换为整形数的除法
//运算结果为0,转换为double后为0.0


double f = 1.0/2;
//由于1.0是浮点型,所以2会发生隐式类型转换变为2.0,最终结果是0.5


int ival;
if(ival) //ival发生隐式类型转换为bool
{

}

C++的类型转换

const_case

  用于转换指针或引用,去掉类型的const属性。

  这几种类型转换,只有const_cast可以将const性质转换掉,其余都不行。相应的,除了添加或删除const属性,const_cast也无法执行其他的类型转换任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//const_case使用

int main()
{
// C++ const_cast
const int a = 10;
//int* pA = &a; //const int*类型不能用于初始化int*类型实体
int* pA = const_cast<int*>(&a);
*pA = 100;

//需要注意,虽然我们用指针把a的值改变了,实际上也确实改变了
//但假如我们要使用a的值,比如cout一下,那么a原来的值(10)会直接赋值到行为上
//cout<<a<<endl;输出的结果是10,而不是100
return 0;
}

reinterpret_cast

  这是一种很危险的类型转换。既不检查指向的内容,也不检查指针类型本身,只是做了类型的重新解释。reinterpret_cast需要保证转换前后的类型所占用的内存大小一致,否则将引发编译时错误。

  虽然危险,但工程中的使用常见还是比较广泛,比如void*和其他类型的转换,遇到这种场景最好使用reinterpret_cast而不是C语言的强制类型转换,因为C语言的强制转换不做任何的检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//reinterpret_cast使用

int Test()
{
return 0;
}

int main()
{
// C++ reinterpret_cast
typedef void(*FuncPtr) (); //定义函数指针,返回值void,参数也是void

FuncPtr funcPtr;
//funcPtr = &Test; //两个函数模型不匹配,不能赋值
funcPtr = reinterpret_cast<FuncPtr>(&Test);

return 0;
}

static_case和dynamic_cast

  static_cast用于基本的类型转换(这种方式就是类似于C语言的类型转换方式)。仅当类型之间可隐式转换时,static_cast的转换才是合法的,否则将出错,比如基本类型的指针之间不含有隐式转换,那么使用static_cast进行显式转换将编译错误。

  static_cast也可以用于有继承关系类对象和类指针之间的转换,但需要由程序员来确保转换是安全的,它不会产生动态转换的类型安全检查的开销,因为类型检查不是编译器能检测出来的,必须要等到运行时才能动态检查。

  dynamic_cast是为了弥补static_cast的不足,可以做类型的检查。类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,因此它只能用于含有虚函数的类,必须用于多态体系中,用于类层次间的向上和向下转化;向上转化时和static_cast效果相同,向下转化时,如果是非法的对于指针返回NULL,引用抛出bad_cast异常(向上转化:子类转换为父类;向下转化:父类转化为子类。子类转化为父类比较安全,但父类转化为子类不安全,因为子类一定有父类的属性,但父类未必有子类的属性)。

  dynamic_cast只能用于类的指针、类的引用或void*。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//static_cast 和 dynamic_cast

class Base
{
public:
Base() : _i(0) { ; }
virtual void T() { cout << "Base:T" << _i << endl; } //必须有虚函数,dynamic_cast才做检查
private:
int _i;
};

class Derived : public Base
{
public:
Derived() :_j(1) { ; }
virtual void T() { cout << "Derived:T" << _j << endl; }

private:
int _j;
};

int main()
{
// static_cast
int i = 6;
double d = static_cast<double>(i); //基本类型转换 int -> double
double d2 = 5.6;
int i2 = static_cast<int>(d2); //基本类型转换 double -> int

int ii = 5;
double dd = static_cast<double>(ii);
double dd2 = 5.6;
int ii2 = static_cast<int>(dd2);

// static_cast与dynamic_cast

Base cb;
Derived cd;
Base* pcb;
Derived* pcd;

// 子类--》 父类
// 这个是安全的
pcb = static_cast<Base*>(&cd);
if (pcb == NULL)
{
cout << "unsafe static_cast from Derived to Base" << endl;
}
pcb = dynamic_cast<Base*>(&cd);
if (pcb == NULL)
{
cout << "unsafe dynamic_cast from Derived to Base" << endl;
}

// 父类--》 子类
// 这个有风险,dynamic_cast会做检查导致失败
pcd = static_cast<Derived*>(&cb);
if (pcd == NULL)
{
cout << "unsafe static_cast from Base to Derived" << endl;
}
pcd = dynamic_cast<Derived*>(&cb);
if (pcd== NULL)
{
cout << "unsafe dynamic_cast from Base to Derived" << endl;
}

return 0;
}

  为了避免一个文件被多次include,有两种方式:

  • 使用宏来防止同一个文件被多次包含
    • 优点:可移植性好
    • 缺点:无法防止宏名重复,难以排错
1
2
3
4
#ifndef _SOMEFILE_H_
#define _SOMEFILE_H_

#endif

  

  • 使用编译器来防止同一个文件被多次包含
    • 优点:可以防止宏名重复,易排错
    • 缺点:可移植性不好,windows支持,其他平台未必
1
#pragma once