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

指针

参数传递

下列程序是否正确:

1
2
3
4
5
6
7
8
9
10
11
void GetMemory(char *p)
{
p = new char[100];
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}

错误,程序崩溃。因为GetMemory 并不能传递动态内存, Test 函数中的 str 一直都是 NULL。strcpy(str, "hello world");将使程序崩溃。

1
2
3
4
5
6
7
8
9
10
11
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}

可能返回乱码。因为GetMemory 返回的是指向“栈内存”的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。

1
2
3
4
5
6
7
8
9
10
11
void GetMemory2(char **p, int num)
{
*p = new char[num];
}
void Test(void)
{
char *str = NULL;
GetMemory2(&str, 100);
strcpy(str, "hello");
printf(str);
}

能正常运行,但是内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
oid Test(void)
{
char *str = new char[100];
strcpy(str, "hello");
delete[ ] str;
if (str != NULL)
{
strcpy(str,"world");
printf(str);
}
}

篡改动态内存区的内容,后果难以预料,非常危险。因为 delete[ ]str;之后,str成为野指针(需要str = NULL;)if(str != NULL)语句不起作用。

编译

编译选项MT和MD的区别

  在C++(特别是Windows平台下的MSVC编译器)中,/MT 和 /MD 是两种不同的运行时库链接方式,主要区别在于如何链接C/C++标准库(如 msvcrt.lib、libcmt.lib 等)。

  • /MT:静态链接运行时库

  全称是Multi-Threaded(静态多线程库)。特点是将C/C++标准库(如libcmt.lib)静态编译到你的程序中,生成的可执行文件不依赖外部的运行时库DLL,不过因为库代码被直接嵌入导致生成的文件体积较大。适用的场景有需要独立分发的程序(需要避免系统依赖),或兼容性要求高(避免用户缺少运行时库)。

  • /MD:动态链接运行时库

  全称是Multi-Threaded Dll(动态多线程库)。特点是程序动态链接到微软的运行时库DLL(如msvcrXX.DLL),生成的文件体积较小,但运行时需要系统中有对应的DLL,通常需要程序分发对应的vcredist(微软VisualC++运行时库)。适用的场景有需要多个程序共享同一份运行库(减少磁盘和内存占用),或依赖其他动态库时,保持运行时版本库一致。

需要注意的问题:

  1. 如果项目中的库(如第三方的DLL)和主程序使用不同的选项(一个用/MT,一个用/MD),会导致链接冲突或运行时崩溃。需要保证所有模块(包括exe和dll)使用相同版本的运行时库选项。
  2. /MTd和/MDd是对应的调试版本,分别链接到静态(如libcmtd.lib)或动态(如msvcrXXd.dll)的调试运行库。
  3. 现代大多数情况下推荐使用/MD动态链接,便于更新运行时库,只有特殊需求(如嵌入式环境)才使用/MT。

编程

不用C++库函数,自行实现strcpy

1
2
3
4
5
6
7
8
char *strcpy(char *strDest, const char *strSrc);
{
assert((strDest!=NULL) && (strSrc !=NULL));
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘\0’ )
;
return address ;
}

  泛型编程就是以独立于任何特定类型的方式编写代码。使用泛型程序时,我们需要提供具体程序实例操作的类型或只值。

  泛型编程和面向对象编程一样,都依赖于某种形式的多态性。

  面向对象编程中的多态性在运行时依赖存在继承关系的类。我们能够编写使用这些类的代码,忽略基类与派生类之间类型上的差异。只要使用基类的引用或指针,基类类型或派生类类型的对象就可以使用相同的代码。

  如果说面向对象是一种通过间接层来调用函数以换取一种抽象(创建一个接口类),那么泛型编程则是更直接的抽象,它不会因为间接层而损失效率。不同于面向对象的动态期多态,泛型编程是一种静态期多态,通过编译器生成最直接的代码。泛型编程可以将算法与特定类型、结构剥离,尽可能复用代码。标准库中的容器、迭代器和算法是很好的泛型编程的例子,几乎可以在任意类型上使用标准库的类和函数。

  C++中,模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。

函数模板

  模板定义以关键字template开始,后接模板形参表。模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。

  模板形参表很像函数形参表,函数形参表定义了特定类型的局部变量但并不初始化那些变量,在运行时再提供实参来初始化形参。同样,模板形参表示可以在类或函数的定义中使用的类型或值。

1
2
3
4
5
6
7
//声明一个名为T的类型形参
//在callWithMax内部,可以使用名字T引用一个类型,具体表示哪个类型由编译器根据所用函数参数而确定
template<typename T>
void callWithMax(const T &a, const T &b)
{
f(a > b ? a : b);
}

  类型形参T跟在关键字class或者typename之后定义,这里class和typename没有区别。模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。模板形参选择的名字没有本质含义。可以给模板形参赋予的唯一含义是区别形参是类型形参还是非类型形参。如果是类型形参,我们就知道该形参表示的是未知类型;如果是非类型形参,我们就知道他是一个未知值。模板非类型形参是模板定义内部的常量值

  上面那个模板函数若再加上inline(不要放在template之前,要放在模板形参之后,返回类型之前),使其成为模板内联函数。这个模板内联函数可以用下列宏定义替换:

1
2
3
4
5
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //这里a被累加两次
CALL+WITH_MAX(++a, b + 10); //这里a被累加一次

  由此可见,用模板内联函数可以避免宏定义中犯人的括号。而且还可以避免莫名其妙的情况,上面例子可见第一次运算的时候a的递增次数竟然依赖于它被拿来和谁比较。因此我们应该尽量少使用宏!!

  (宏定义和内联的区别:宏定义是在预处理阶段进行代码替换,内联函数是在编译阶段插入代码;其次宏没有类型检查,函数由类型检查)

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
/*
泛型编程的主要工作不是程序员,而是编译器
编译器会在编译期间根据代码自动生成方法
由于大量工作是编译器操作,可能通用的泛型函数并不能满足我们的需求
所以我们通常还需要“特化”处理
*/


//特化
//如果是字符串的话特殊处理
//其余的输入可以让输入类型不相同,返回值统一为int
template<>
char* max(char* a, char* b)
{
return (strcmp(a, b) > 0 ? (a) : (b));
}
template<class T1, class T2>
int max(T1 a, T2 b)
{
return static_cast<int>(a > b ? a : b);
}

#include <iostream>
using namespace std;
int main()
{
//调用函数
char* s1 = "hello";
char* s2 = "world";
cout << max(s1, s2) << endl;
cout << max(2, 4.5) << endl;
}

  泛型编程是把算法和具体的数据结构分开了,我们不需要考虑类型本身是什么,直接用一套逻辑把所有的类型都涵盖了,如果需要针对某些特殊类型做处理,我们就进行单独的“特化”。这里比较复杂的操作是编译器的推理过程,程序员所做的工作无非是把该定义好的类型通知编译器,让编译器帮助我们做处理。

类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
template<class Type>
class Queue
{
public:
Queue();
Type &front();
const Type &front() const;
void push(const Type&);
void pop();
bool empty() const;
};

Queue<int> q1; //必须显式指定实参

  使用类模板时,必须为模板形参显式指定实参,编译器使用实参来实例化这个类的特定类型版本,重新编写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
/*
泛型编程的思想可以做一些算法优化,比如下面的等差数列求和
*/

// 1+2+3...+100 ==> n*(n+1)/2

//这里用泛型不是用泛型编程可以传递广泛类型的属性,这里直接定义了int
//这里是借用泛型编程的另一个特征:编译期推理,让程序自动生成代码
template<int n>
struct Sum
{
// 递推的思路,求前n个数的和可以表示为前(n-1)个数的和+n
// Sum(n) = Sum(n-1)+n
// 这里声明一个enum的成员,内部的枚举值是N,N的计算方法是递归求和
// 利用泛型编程的自动推理完成编译期计算
enum Value {
N = Sum<n-1>::N+n
};
};
//下面这个就是递推的基准点,n=1的特化
// 如果不设置,那么就会无穷递归
template<>
struct Sum<1>
{
enum Value {N = 1}; // n=1
};

int main()
{
//这里只需要输出结果就行,运算过程中在编译期就完成了
cout << Sum<100>::N << endl; //计算1+2+……+99+100
return 0;
}

  模板编程的难点很大程度上在于对编译器的理解,我们需要直到怎么帮助编译器提供需要生成代码的信息。

设计模式

  一个模式描述了一个不断发生问题及这个问题的解决方案;模式是前人的设计经验上总结出来的对于一些普遍存在的问题提供的通用解决方案,比如单例模式、观察者模式等;

  软件工程中有很多模式,面向对象常见的有23种设计模式,可以分为创建型,结构型和行为型(观察者)模型。创建型模型是处理对象创建的设计模式,试图根据实际情况使用合适的方式创建对象,又两个主导思想:一是将系统使用的具体类型封装起来,二是隐藏这些具体类的实例创建和结合的方式。创建型模式包括单例模式、抽象工厂模式、建造者模式、工厂模式,原型模式。结构型模式有适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式代理模式。行为型模式有模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、职责链模式、访问者模式。

  设计模式不是万能的,它建立在系统变化点上,哪里有变化哪里就可以用。设计模式是为了解耦合,为了扩展,它通常是演变过来的,需要演变才能准确定位。设计模式是一种软件设计方法,不是标准,面目前大部分框架中都包含了大量的设计模式思想。

  

单例模式

  有些时候,我们需要整个程序中有且只有一个实例,比如系统日志、Windows资源管理器窗口,数据库分配主键等操作。单例的实现思路:

  1. Singleton拥有一个私有的构造函数,确保用户无法通过new直接实例它;
  2. 包含一个静态私有成员变量instance与静态公有方法Instance(),让外部通过Instance()来获取实例;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Singleton.h

class Singleton
{
public:
//只有静态的方法才能访问静态的变量
static const Singleton* getInstance();
static void DoSomething()
{
cout << "Do Something" << endl;
}
// 将构造和析构函数私有化,防止外部访问
private:
Singleton();
~Singleton();

// 使用静态变量帮助解决资源的分配和释放
// 肯定不能使用栈上的对象,不然会销毁
// 如果使用堆上的对象,在getInstance()方法中new出来,那就涉及到资源释放问题
// 析构函数是private的,外部是无法直接释放的
// 静态变量在程序中属于全局区,但声明了private后可见区域只在类内
// 所以可以让这个实例随着程序的产生而产生,随着程序的灭亡而灭亡
static Singleton* This;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Singleton.cpp

Singleton* Singleton::This = nullptr; //静态变量需要显式声明


const Singleton* Singleton::getInstance()
{
//这里只是简化了,还需要考虑多线程的情况
if (!This)
{
This = new Singleton;
}
return This;
}

Singleton::Singleton()
{
}

Singleton::~Singleton()
{
}
1
2
3
4
5
6
7
8
int main()
{
Singleton::getInstance()->DoSomething();

Singleton::getInstance()->DoSomething();

return 0;
}

  

  上面的单例模式实现,程序开始时全局实例Singleton* Singleton::This为空,只有我们主动调用Singleton::getInstance()->DoSomething()方法时才会产生这个对象的实例。我们也可以采用饿汉的方式,程序启动时就创建实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Singleton.cpp

Singleton* Singleton::This = new Singleton(); //这里直接new

const Singleton* Singleton::getInstance()
{
//这里只是简化了,还需要考虑多线程的情况
if (!This)
{
This = new Singleton;
}
return This;
}

Singleton::Singleton()
{
}

Singleton::~Singleton()
{
}

  

观察者模式

  在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出相应;对象通常是一对多关系。常见于各种MVC的框架中,Model的变化通知各种类型的View时几乎都存在这种模式。

  观察者模式的实现思路:把问题的职责解耦合,将Observable和Observer抽象开,分清抽象和实体。

1
2
3
4
5
6
7
8
9
10
11
//观察者类

class Observer
{
public:
Observer() { ; }
virtual ~Observer() { ; }

// 当被观察对象发生变化时,通知被观察者调用这个方法
virtual void Update(void* pArg) = 0;
};

用list列表来存储被观察清单。

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
//被观察对象

class Observer;

class Observerable
{
public:
Observerable();
virtual ~Observerable();

// 注册观察者(加入被观察列表)
void Attach(Observer* pOb);
// 反注册观察者
void Detach(Observer* pOb);

int GetObseverCount() const
{
return _Obs.size();
}

void DetachAll() //清理所有订阅者
{
_Obs.clear();
}

virtual void GetSomeNews(string str)
{
SetChange(str);
}
protected:
void SetChange(string news); // 有变化,需要通知

private:
// 通知观察者
void Notify(void* pArg);

private:
bool _bChange;
list<Observer*> _Obs; //观察对象列表
};


// 注册观察者
void Observerable::Attach(Observer* pOb)
{
if (pOb == NULL)
{
return;
}

// 看看当前列表中是否有这个观察者
auto it = _Obs.begin();
for (; it != _Obs.end(); it++)
{
if (*it == pOb)
{
return;
}
}

_Obs.push_back(pOb);
}

// 反注册观察者
void Observerable::Detach(Observer* pOb)
{
if ((pOb == NULL) || (_Obs.empty() == true))
{
return;
}

_Obs.remove(pOb);
}

void Observerable::SetChange(string news)
{
_bChange = true;

Notify( ( (void*)news.c_str() ));
}


void Observerable::Notify(void* pArg)
{
if (_bChange == false)
{
return;
}

// 看看当前列表中是否有这个观察者
auto it = _Obs.begin();
for (; it != _Obs.end(); it++)
{
(*it)->Update(pArg);
}

_bChange = false;
}
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
//main

//被观察对象
class News : public Observerable
{
public:
virtual void GetSomeNews(string str)
{
SetChange("News: " + str);
}
};

//两个观察者
class User1:public Observer
{
public:
virtual void Update(void* pArg)
{
cout << "User1 Got News: " << reinterpret_cast<char*>(pArg) <<endl;
}
};
class User2 :public Observer
{
public:
virtual void Update(void* pArg)
{
cout << "User2 Got News: " << reinterpret_cast<char*>(pArg) <<endl;
}
};

int main()
{
User1 u1;
User2 u2;

News n1;
n1.GetSomeNews("t0"); //此时没人订阅,什么事情都不会发生

//订阅
n1.Attach(&u1);
n1.Attach(&u2);
n1.GetSomeNews("t1");

n1.Detach(&u2);
n1.GetSomeNews("t2");

return 0;
}

  观察者模式帮助我们把职责关系梳理清晰了,我们如果要增加一个新的观察者,直接继承一个Observer类,实现一下消息更新的虚方法即可。

适配器(Adapter)模式

  适配器模式可以理解为一个插口转换器,去不同国家旅游时可以适配不同的插座接口。适配器将类接口转换为客户端期望的另一个接口,让接口更兼容。适配器模式的动机是:如果可以更改接口,则可以重用现有的软件。

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
//适配器模式
//场景:
// 已经有一个矩形处理类LegacyRectangle,可以画一个矩形
// 此时收到需求,在画矩形之前需要额外输入一些别的东西,原有的LegacyRectangle类不能满足需求
//


//原始的矩形处理类
class LegacyRectangle
{
public:
LegacyRectangle(double x1, double y1, double x2, double y2)
{
_x1 = x1;
_y1 = y1;
_x2 = x2;
_y2 = y2;
}

void LegacyDraw()
{
cout << "LegacyRectangle:: LegacyDraw()" << _x1 << " " << _y1 << " " << _x2 << " " << _y2 << endl;
}

private:
double _x1;
double _y1;
double _x2;
double _y2;
};



// 创建一个抽象类和外部对接,让外部去调用这个接口
class Rectangle
{
public:
virtual void Draw(string str) = 0;
};


//创建内部接口,来实现对原始LegacyRectangle代码的复用
//有两种方式:

// 第一种适配的方式:使用多重继承,把原始的类一并继承
class RectangleAdapter: public Rectangle, public LegacyRectangle
{
public:
RectangleAdapter(double x, double y, double w, double h) :
LegacyRectangle(x, y, x + w, y + h)
{
cout << "RectangleAdapter(int x, int y, int w, int h)" << endl;
}

virtual void Draw(string str)
{
cout << "RectangleAdapter::Draw()" << endl;
LegacyDraw(); //实际使用的还是原始的方法
}
};

// 第二种适配的方式:组合方式的Adapter,把原始的类当作成员变量
// 这种方式更常见,因为可以避免多重继承方式的强耦合性
class RectangleAdapter2 :public Rectangle
{
public:
RectangleAdapter2(double x, double y, double w, double h) :
_lRect(x, y, x + w, y + h)
{
cout << "RectangleAdapter2(int x, int y, int w, int h)" << endl;
}

virtual void Draw(string str)
{
cout << "RectangleAdapter2::Draw()" << endl;
_lRect.LegacyDraw();
}
private:
LegacyRectangle _lRect;
};

int main()
{
double x = 20.0, y = 50.0, w = 300.0, h = 200.0;
//实际创建的是适配器对象,但接口调用用的是Rectangle接口类
RectangleAdapter ra(x, y, w, h);
Rectangle* pR = &ra;
pR->Draw("Testing Adapter");

cout << endl;

RectangleAdapter2 ra2(x, y, w, h);
Rectangle* pR2 = &ra2;
pR2->Draw("Testing2 Adapter");

return 0;
}

  计算机程序的输入流起点和输出流的终点都可以是磁盘文件。C++把每个文件都看成是一个有序的字节序列,每个文件都以文件结束标志结束。

  按照文件中数据的组织形式可以把文件分为:

  1. 文本文件:文件中信息形式为ASCII码文件,每个字符占用一个字节;
  2. 二进制文件:文件中信息的形式与其在内存中的形式相同;

  

文件操作步骤:

  1. 打开文件open;
  2. 检查打开是否成功;
  3. 读或写read\write;
  4. 检查是否读完EOF(end of file);
  5. 使用完文件后关闭文件close;
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
//把输入的字符信息写入文件

#include <iostream>
#include <fstream>

using namespace std;

int main()
{
int a;
int index = 0;

fstream fout;
fout.open("test.txt", ios::app);

/*
//可以直接这么写,程序会默认打开
fstream fout("test.txt");
*/

if(fout.fail()) //也可以判断fout是否非空:if(!fout)
{
cout << "open fail" << endl;
}

while(cin >> a)
{
fout << "The numbers are:" << a << endl; //用定义的fout输出到文件
index++;
if(index == 5)
{
break;
}
}

fout.close(); //关闭文件
}

  

文件打开方式 行为
ios::in ifstream的默认模式,打开文件进行读操作
ios::out ofstream的默认方式,打开文件进行写操作
ios::ate 打开一个已经有输入或输出文件并查找到文件尾
ios::app 打开文件以便在文件的尾部添加数据
ios::nocreate 如果文件不存在,则打开操作失败
ios::trunc 如果文件存在,清除文件原有内容(默认)
ios::binary 以二进制方式打开

  

  文件的默认打开方式是ASCII,如果需要以二进制方式打开,需要设置ios::binary:

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
//二进制的方式拷贝一个文件:

#include <string>
#include <fstream>
#include <iostream>
using namespace std;

static const int bufferLen = 2048;
bool CopyFile(const string& src, const string& dst)
{
// 打开源文件和目标文件
// 源文件以二进制读的方式打开
// 目标文件以二进制写的方式打开
ifstream in(src.c_str(), ios::in | ios::binary);
ofstream out(dst.c_str(), ios::out | ios::binary | ios::trunc);

// 判断文件打开是否成功,失败返回false
if (!in || !out)
{
return false;
}

// 从源文件中读取数据,写到目标文件中
// 通过读取源文件的EOF来判断读写是否结束
// 分块读取,不要一下全读进来,防止缓冲区不够用
char temp[bufferLen];
while (!in.eof())
{
in.read(temp, bufferLen);
streamsize count = in.gcount(); //实际读的大小
out.write(temp, count);
}

// 关闭源文件和目标文件
// 如果不关闭会导致资源泄露
in.close();
out.close();

return true;
}

int main()
{
CopyFile("AA.mp3", "BB.mp3");

return 0;
}

I/O流

  传统的C语言中I/O处理有printf,scanf,getch,gets等函数,他们的问题是不可编程,仅仅能识别内置的数据类型,无法识别自定义的数据类型;而且代码移植性差,有很多坑。

  C++中有I/O流istream,ostream等处理方式,可编程,对于类库的设计者来说很有用;而且还能简化编程,使I/O的风格保持一致。

IO缓存区

  计算机发展过程中,需要实现外部的设备可以用统一的标准和计算机程序进行交互。最早的UNIX系统是以文件的方式进行交互。这个思想发展到现在就是IO缓存区。因为外部设备和内存的速度是不一样的,缓存区的存在可以让我们的信息读取更加高效。

标准的IO提供三种类型的缓存模式:

  1. 按块缓存:如文件系统,一次性加载到内存中
  2. 按行缓存:以''区分行
  3. 不缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
int a;
int index = 0;

while(cin >> a)
{
cout << "The numbers are:" << a << endl;
index++;
if(index == 5)
{
break;
}
}

char ch;
cin >> ch;
cout << "the last char is:" << ch << endl;

return 0;
}

  上面代码一连输入6个int,内部接收完5个数字后循环结束,但可以看到6还是读入了,并且以char的形式输出,后面的ch还没来得及输入程序就退出了。这是程序中面临的一个问题,输入的6被暂存到缓冲区中成为了脏数据,在我们不之情的情况下把后面的输入给覆盖掉了。

我们可以使用cin::ignore(int count,type metadelim)方法来清空缓冲区,第一个参数是要清空多少缓冲区信息,第二个参数表达以什么符号作为结尾

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
//清空缓冲区
int main()
{
int a;
int index = 0;

while(cin >> a)
{
cout << "The numbers are:" << a << endl;
index++;
if(index == 5)
{
break;
}
}

//清空缓冲区脏数据
//numeric_limits<std::streamsize>::max()表示缓冲区的最大范围
cin.ignore(numeric_limits<std::streamsize>::max(), '\n');

char ch;
cin >> ch;
cout << "the last char is:" << ch << endl;

return 0;
}