C++程序基础
1. 程序运行过程及内存分布
(1)程序运行过程
我们使用指定c++编译器(windows环境下的vs编译器为例)写完一段代码后,通过编译生成.exe文件(我们需要的可执行文件),那编译过程又是怎样的呢?如果是在linux环境下编程,这个过程就比较明朗,需要我们自己手动编写makefile,里面关于编译,链接的逻辑就很清晰。换在windows环境下,本来也需要写makefile,但是VS编译器帮我们自动生成了makefile,不需要我们关心生成过程即可得到我们需要的目标文件。为了更好地理解代码地运行,个人觉得还是有必要了解程序的生成可执行文件,再到程序执行的过程(windows),如图:
从整个编译过程中,我们可以看到如下发现:
-
- 预处理:不会进行语法分析,类型检查等,而C++宏泛滥加剧了代码维护的成本,虽提出const解决类型检查的方案,但不解决根本问题。还好C++20提出Module用来摆脱丑陋的宏方法。
- :
- 我们知道"编译"是将工程中所有的源文件(.cpp)分开单独进行编译,最后再"链接"成可执行文件,这也就意味着".h"文件不参与编译。但我们一般会用".cpp"文件包含".h"文件,在"预处理"阶段进行展开,也就是 将".h"文件的内容插到".cpp"里,从而理论上实现".h"文件内容的”编译“;
- 然后我们再看看".cpp"编译时做了些啥?编译器在编译成员函数定义时,首先从展开的".h"文件的内容里找到函数原型,然后分析函数定义里的语法,词法,如果没有问题再将函数定义拷贝到”代码区“。这也就意味着展开的的".h"文件的内容只是用来查找成员函数原型和成员变量类型,并不参与实际的编译工作,也就是说".h"文件里的函数定义是不参与编译的。(eg.你可以在.h文件里定义一个函数,然后写入inta2 i;如果不调用这个函数,编译器不会报错);
- 如2所说,".h"文件是不参与编译的,那为什么".h"里的函数定义在程序执行时又可以被调用呢?这里又引出一个新的概念"运行时编译",当我们编译完成后,生成一个可执行程序,当我们运行程序时,就会找到主函数的入口地址,代码从这里开始执行,当我们调用".h"里函数申明和函数定义在一起的函数时,编译器会把".h"的函数定义在调用处展开,并对函数体的代码进行语法分析,没有问题后再将函数定义拷贝到”代码区“,本质就是过程2,也就是编译过程。最后,程序会跳转到函数的入口地址执行。
(2)程序的内存分布
我们在最开始学习C++程序时遇到的最大敌人应该算是"内存泄露"了,特别是项目较大时,往往需要花费很大的精力在此处。虽然C++11提出了“智能指针”来解决内存泄露问题,但是如果你用不好智能指针,一样会造成内存泄露(eg.shared_ptr在多线程中使用时计数器管理不当)的问题。我们有必要了解程序的内存分布情况,帮助我们更好地解决以后程序在内存出现的各种状况。
-
- 熟悉了程序的内存分布,我们还需要注意一些问题:
- 堆的生长方向是向上的,也就是向着内存地址增加的方向,而栈刚好是相反的,栈是向着内存减小的方向生长的(因为栈的空间十分有限,所以栈是从上限往栈的下限生长的)。
-
一个线程上的栈内存是有限的,通常为8MB左右(大小取决于运行环境),栈上的内存通常是由编译器自动管理的。当在栈上分配一个新的变量时,或进入一个函数时,栈的指针会向下移动(下压栈),相当于在栈上分配了一块内存。我们把变量分配在栈上,也就是利用了栈上的内存空间,当这个变量的生命周期结束的时候,栈的指针会上移,相当于回收了此块内存。
2. 程序执行入口
(1)main函数说明
-
- CUI(控制台) 入口函数:main
- GUI(win32) 入口函数:WinMain
- Dll(动态库) 入口函数:DllMain
但是,那为什么我们使用VS编译器新建一个CUI程序,主函数是_tmain()呢?下面我们将分析原因。
(2)字符集
随着我们的学习,经常会遇到ANSI窄字节和Unidoce宽字节的处理,应该很多人为此头疼过吧,关于具体的处理可参考 windows字符串,回来我们再看_tmain()是怎么回事?看看“_t”的通用设定,最典型的例子就是MFC的CString类, 我们在传字符串时,都会加上_T(""). 下面我们就拿_tprintf()进行说明
3. 变量及变量类型
(1)变量命名规则
关于变量的命名规则,在windows下我们比较常见的像匈牙利命名法,在linux下则喜欢使用下划线连接的方式。额,这个应该不重要吧,我看C的书上面x,y,z,i什么的也没影响啊?下面我们说说理由:
-
- C++的OOP核心就是为了让程序的层次和逻辑更直观地呈现在设计者面前,程序的可读性和可维护性成为项目最基础的要求,而不再是一个文件 一个函数去实现一整个项目的所有功能;
- 作为初学者不太会有意识地去按照某个特定的规则是去命名这本身无可厚非,毕竟代码更多是自己在阅读。但是一旦进入到项目进入大团队,你需要与上,下位机进行联调时,你的职业有几个储备资源,别人要使用你的代码时,“哇,真是糟糕的代码,这个变量也不知道干啥用的”。所以很多项目负责人都会推行一套符合现阶段项目需求的命名规范及代码设计规则,保证后期的代码的可维护性及某个岗位离职时交接代码的可读性。
(2)变量在内存中大小
类型 | 大小(字节) | 特别情况说明 |
bool | 1 | |
char | 1 | |
short | 2 | |
int | 4 | |
long | 4 | |
long long | 8 | |
float | 4 | |
double | 8 | |
字符串 | strlen(字符串) |
字符串以'/0‘结尾,所以sizeof()大小比实际字符串大1,为此专门设计strlen()来获取字符串大小 |
指针 | 4 | 指针本身就是内存块上的一个32位地址,所以大小都是4,除了定义的数组指针除外 |
数组指针 | 数组元素个数 * sizeof(元素) | 为了方便计算数组所占内存大小,sizeof(定义的数组名)可获得相应数组大小 |
结构体 | sizeof(结构体) | VS编译器默认为4字节对齐,可以使用#pragma pack(n)来指定内存对齐方式 |
零长数组 | 0 | GUN支持,但VS编译器会提示警告,上下位机的数据传输中用于协议的制定 |
联合体 | sizeof(联合体所有元素中所占内存最大的) |
-
- 为了更方便理解,给出以下代码进行说明
1 #include "stdafx.h" 2 #include "string.h" 3 4 int _tmain(int argc, _TCHAR* argv[]) 5 { 6 printf("bool: %d\n", sizeof(bool)); //1 7 printf("char: %d\n", sizeof(char)); //1 8 printf("short: %d\n", sizeof(short)); //2 9 printf("int: %d\n", sizeof(int)); //4 10 printf("long: %d\n", sizeof(long)); //4 11 printf("long long:%d\n", sizeof(long long));//8 12 printf("float: %d:\n", sizeof(float)); //4 13 printf("double: %d\n", sizeof(double)); //8 14 printf("=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+\n"); 15 16 //字符串 17 const char* pcData = "abcdef"; 18 printf("str: %d\n", sizeof(pcData)); //4 19 printf("str: %d\n", sizeof("abcdef")); //7 20 printf("str: %d\n", strlen("abcdef")); //6 21 printf("=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+\n"); 22 23 //指针 24 int iData = 4; 25 int aiData[6] = {1,2,3,4,5,6}; 26 int *pcPoint = aiData; 27 printf("int: %d\n", sizeof(iData)); //4 28 printf("array: %d\n", sizeof(aiData)); //4 * 6 = 24 29 printf("array point: %d\n", sizeof(pcPoint));//4 30 //函数指针 31 using pfnPoint = void(*)(); 32 printf("pfn: %d\n", sizeof(pfnPoint)); //4 33 printf("=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+\n"); 34 35 //结构体 36 typedef struct 37 { 38 short sData; 39 long lData; 40 }ST_DATA, *PST_DATA; 41 printf("struct: %d\n", sizeof(ST_DATA)); //8 42 printf("struct point: %d\n", sizeof(PST_DATA));//4 43 44 //修改编译器的内存对齐,默认为4字节对齐 45 #pragma pack(1) 46 typedef struct 47 { 48 char cData; 49 long lData; 50 }ST_MEM, *PST_MEM; 51 #pragma pack() 52 printf("struct mem: %d\n", sizeof(ST_MEM)); //5 53 printf("struct mem point: %d\n", sizeof(PST_MEM));//4 54 55 //GUN支持,VS不支持 56 typedef struct 57 { 58 int iData; 59 char acData[0]; 60 }ST_ARRAY, *PST_ARRAY; 61 printf("struct array: %d\n", sizeof(ST_ARRAY)); //4 62 printf("struct array point: %d\n", sizeof(PST_ARRAY));//4 63 64 //联合体 65 typedef union 66 { 67 char cData; 68 long lData; 69 }UN_DATA, *PUN_DATA; 70 printf("union: %d\n", sizeof(UN_DATA));//4 71 printf("union point: %d\n", sizeof(PUN_DATA));//4 72 printf("=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+\n"); 73 74 return 0; 75 }
(3)数值类型后缀
当我们在C++ 中 写一个整型数值时,编译器会自动的将其识别为 int 类型,同样的当我们写一个小数时,编译器会自动的将其识别为double 类型,但如果想指定特定类型时该怎么办呢,加表示类型的 ”后缀“
后缀名 | 含义 | 说明 |
---|---|---|
u | unsigned | auto atData = 10u |
l | long/long double | auto atData = 10l |
ll | long long | auto atData = 10ll |
ul | unsigned long | auto atData = 10ul |
ull | unsigned long long | auto atData = 10ull |
f | float | auto atData = 3.0f |
i[n] | i8(char), i16(short), i32(int), i64(__int64) | auto atData = 10i8 |
ui[n] | ui8(unsigned char), ui16(unsigned short), ui32(unsigned int), ui64(unsigned __int64) | auto atData = 10ui8 |
(4)算术运行符和逻辑运算符
在C++里常用的算术运行符有+,-,*,/,%,++,--,除此之外<<, >>, &, |,在数据处理时也经常会用到。而逻辑运行符!,&& ,||, !=, ==, ?=主要用在条件表达(if, while,switch等)中。
算术运算符 | ||
---|---|---|
类型名称 | 类型 | 特殊说明 |
加 | + | |
减 | - | |
乘 | * | |
除 | / | |
取余 | % | |
位移 | <<, >> | 将数字转化成二进制数,左移<<相当于往右边加个0,乘以2;右移>>相当于往左边加个0,除以2 |
与 | & | |
或 | | | |
自加 | ++ | 加号在前,先赋值再运算;否则,先运算再赋值 |
自减 | -- | 减号在前,先赋值再运算;否则,先运算再赋值 |
逻辑运算符 | ||
非 | ! | |
逻辑与 | && | |
逻辑或 | || | |
相等 | == | |
不等 | != | |
三目运算符 | ? = | eg, int iData = bIsTrue ? 4 : 6; 如果bIsTrue为真,则iData = 4; 否则iData = 6. |
大于 | > | |
小于 | < | |
大于等于 | >= | |
小于等于 | <= |
-
- 为了方便大家理解,这里给出代码进行说明
1 #include "stdafx.h" 2 #include "string.h" 3 #include <array> 4 5 int _tmain(int argc, _TCHAR* argv[]) 6 { 7 int iData = 3; 8 int iTemp = 4; 9 10 //算术运算符 11 int iSum = iData + iTemp; 12 printf("iSum: %d\n", iSum); //7 13 int iReduce = iData - iTemp; 14 printf("iReduce: %d\n", iReduce); //-1 15 int iRide = iData * iTemp; 16 printf("iRide: %d\n", iRide); //12 17 int iExcept = iTemp / iData; 18 printf("iExcept: %d\n", iExcept); //1 19 int Surplus = iData % iTemp; 20 printf("iSurplus: %d\n", Surplus); //3 21 int iMoveLeft = iTemp << 1; 22 printf("iMoveLeft: %d\n", iMoveLeft); //8 23 int iMoveRight = iTemp >> 1; 24 printf("iMoveRight: %d\n", iMoveRight); //2 25 int iAnd = iData & iTemp; 26 printf("iAnd: %d\n", iAnd); //0 27 int iOr = iData | iTemp; 28 printf("iOr: %d\n", iOr); //7 29 int iAddSefAfter = iData++; 30 printf("iAddSefAfter: %d\n", iAddSefAfter); //3 31 int iAddSefFront = ++iData; 32 printf("iAddSefFront: %d\n", iAddSefFront); //5 33 int iSubSefAfter = iData--; 34 printf("iSubSefAfter: %d\n", iSubSefAfter); //5 35 int iSubSefFront = --iData; 36 printf("iSubSefFront: %d\n", iSubSefFront); //3 37 printf("=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+\n"); 38 39 //逻辑运算符 40 bool bData = false; 41 bool bTemp = true; 42 43 if (!bData) { printf("bData\n"); } 44 if (bTemp) { printf("bTemp\n"); } 45 if (!bData && bTemp) { printf("bData && bTemp\n"); } 46 if (bData || bTemp) { printf("bData || bTemp\n"); } 47 if (bData == bTemp) 48 { 49 printf("bData == bTemp\n"); 50 } 51 else if (bData != bTemp) 52 { 53 printf("bData != bTemp\n"); 54 } 55 int iGetInfo = bData ? 4 : 6; //三目运算符 56 57 //简单结合 58 std::array<int, 10> arCompareData = {1,2,3,4,5,6,7,8,9,10}; 59 60 for (auto atData : arCompareData) 61 { 62 if (0 != (atData & 0x04)) 63 { 64 printf("atData: %d\n", atData); 65 } 66 } 67 68 return 0; 69 }
(5)类型转换
一般我们在指针传递或参数传递时,经常会用到“类型转换”来进行处理,而且我们也喜欢使用粗暴的方式对其进行强制转换,最后的结果往往就是精度丢失而造成各种问题,C++为此也作了很多努力,比如我们常见的4种类型转换方式,可以参考C++的类型转换说明。
4. 复合类型
(1)数组
数组是一组在内存上连续的存储相同类型元素的顺序集合。因此,我们在处理数据时常常要用到它,那下面我们就好好剖析下它的一些用法。
-
- 一维数组:
1 #include "stdafx.h" 2 #include <array> 3 4 5 int _tmain(int argc, _TCHAR* argv[]) 6 { 7 //=========================================================数组的创建 8 9 //栈上创建 10 char acArrayDefine[10] = { 0 };//创建数组时不建议申明,即使定义时不需要填充元素,也要初始化为0,确保数据安全 11 12 //堆上创建 13 char* pcArrayDefine = new char[10]; 14 memset(pcArrayDefine, 10, 0);//初始化数组,确保数组安全 15 //... 使用中 16 if (nullptr != pcArrayDefine) delete[]pcArrayDefine;//堆上申请的内存需要手动释放 17 18 //==========================================================数组元素访问,赋值,轮询 19 20 int aiArrayElement[6] = { 3, 4, 2, 1, 5, 7 }; 21 //数组元素的访问 22 //下标访问 23 int iElement = aiArrayElement[0];//下标从"0"开始记数 24 printf("arrayelement 0: %d \n", iElement); 25 26 //指针访问 27 int* piElement = aiArrayElement; 28 printf("arrayelement point 5: %d \n", *(piElement + 5));//指针按元素类型偏移,在内存上偏移量为5 * sizoef(int)字节 29 30 //元素赋值 31 aiArrayElement[0] = 10; 32 printf("arrayelement 0: %d\n", aiArrayElement[0]); 33 34 //获取数组大小 35 //只有定义数组的指针才能获取数组的大小 36 printf("arrayelement total size: %d, piArray: %d \n", sizeof(aiArrayElement), sizeof(piElement)); 37 38 //获取元素个数 39 char acArray[] = { 1, 2, 3 }; 40 printf("array element num: %d\n", sizeof(acArray) / sizeof(acArray[0])); 41 42 //元素轮询 43 for (int iCount = 0; iCount < sizeof(aiArrayElement) / sizeof(aiArrayElement[0]); iCount++) 44 { 45 printf("array poll: %d\n", aiArrayElement[iCount]); 46 } 47 48 //数组指针与数组名 49 char acArrayName[10] = { 0 }; 50 char* pcArrayPoint = acArrayName; 51 52 printf("sizeof(acArrayName): %d sizeof(pcArrayPoint):%d\n", sizeof(acArrayName), sizeof(pcArrayPoint));//10 4 53 printf("acArrayName:%p &acArrayName:%p &pcAArrayPoint:%p &acArrayName[0]:%p\n", acArrayName, &acArrayName, pcArrayPoint, &acArrayName[0]);//全部为#009AFC0C 54 55 return 0; 56 }
-
- 二维数组:二维可以看成是多个一维数组的集合,这样就更好理解了,这里用代码进行简单说明
1 #include "stdafx.h" 2 #include "string.h" 3 #include <array> 4 5 int _tmain(int argc, _TCHAR* argv[]) 6 { 7 //这样就很好理解了:集合里有4个一维数组,每个一维数组里有3个元素,这样就很好理解了 8 char acArray[4][3] = 9 { {1, 3, 4}, 10 {2, 4, 5}, 11 {6, 7, 8}, 12 {6, 9, 8} 13 }; 14 15 //缺省创建 16 char acArrayDefault[][4] = {0}; 17 18 //元素的访问:先找到需要访问的一维数组,再找一维数组里的元素下标 19 printf("acArray element: %d \n", acArray[2][1]); 20 21 //元素轮询 22 for (int iRow = 0; iRow < sizeof(acArray) / sizeof(acArray[0]); iRow++) 23 { 24 for (int iColumn = 0; iColumn < sizeof(acArray[0]) / sizeof(acArray[0][0]); iColumn++) 25 { 26 printf("acArray element poll: %d\n", acArray[iRow][iColumn]); 27 } 28 } 29 30 return 0; 31 }
(2)容器与STL
-
- array:相对于传统的数组方法,array表现得更为灵活,且具有容器的大部分特性,可以说是传统数组的加强版,在某些方面很实用,下面用代码进行说明
1 #include "stdafx.h" 2 #include "string.h" 3 #include <array> 4 #include <vector> 5 #include <map> 6 7 int _tmain(int argc, _TCHAR* argv[]) 8 { 9 10 //创建 11 std::array<char, 10> arArray = { 4, 2, 6, 8, 9, 1, 12, 4, 9, 3 }; 12 13 //================================================元素赋值与访问 14 //原有方式 15 arArray[1] = 5; 16 17 //容器方式 18 arArray.at(0) = 7;//资料上说会检测数字越界,实际测试与[]方法无异 19 //arArray[11] = 10; 20 //arArray.at(11) = 10; 21 22 //===================================================元素的遍历 23 //原有的遍历 24 for (_TUCHAR ucCount = 0; ucCount < arArray.size(); ucCount++) 25 { 26 printf("arArray element old: %d\n", arArray.at(ucCount)); 27 } 28 29 //新方法 30 for (auto atElement : arArray) 31 { 32 printf("arArray elment new: %d\n", atElement); 33 } 34 printf("=====================================================\n"); 35 36 //auto 与 auto & 区别 37 for (auto atElement : arArray)//拷贝副本 38 { 39 if (3 == atElement) atElement = 90; 40 } 41 printf("auto element : %d\n", arArray.at(9)); //3 42 43 for (auto & atElement : arArray)//引用本身 44 { 45 if (3 == atElement) atElement = 90; 46 } 47 printf("auto & element : %d\n", arArray.at(9));//90 48 49 printf("=====================================================\n"); 50 51 //排序 52 //std::sort(arArray.begin(), arArray.end());//默认为升序,下面的lambda表达式后面会说明 53 std::sort(arArray.begin(), arArray.end(), [](int x, int y){ return x > y; }); 54 for (auto atElement : arArray) { printf("sort arArray element: %d\n", atElement); } 55 56 return 0; 57 }
-
- vector:通过上面的介绍,我们总结可知道,传统数组与array都是定长数组,在定义时就决定了其大小,但实际上很多场景上,我们需要根据实际需求来决定数组的大小,那么vector就很好地解决了我们的困境
1 #include "stdafx.h" 2 #include "string.h" 3 #include <array> 4 #include <vector> 5 6 int _tmain(int argc, _TCHAR* argv[]) 7 { 8 std::vector<int> vecArray; 9 10 //添加元素 11 for (int iCount = 0; iCount < 10; iCount++) 12 { 13 vecArray.push_back(iCount); 14 } 15 16 //访问元素 17 printf("vecArray element: %d\n", vecArray.at(1)); 18 19 //遍历元素 20 for (auto atElement : vecArray) { printf("vecArray element poll: %d\n", atElement); } 21 printf("=============================================================================\n"); 22 23 //删除元素 24 vecArray.erase(vecArray.begin() + 0); 25 vecArray.erase(vecArray.end() - 3, vecArray.end());//删除某个区域的数据 26 for (auto atElement : vecArray) { printf("vecArray element poll: %d\n", atElement); } 27 printf("=============================================================================\n"); 28 29 //插入元素 30 vecArray.insert(vecArray.begin() + 2, 20); 31 for (auto atElement : vecArray) { printf("vecArray element poll: %d\n", atElement); } 32 printf("=============================================================================\n"); 33 34 //清空元素 35 vecArray.clear(); 36 printf("vecArray element num: %d\n", vecArray.size()); 37 38 return 0; 39 }
-
- map:前面我们学习了定长数组array和变长数组vector,但有时候我们的需求想要像pyhon的字典一样,可以更方便地访问我们想要的数据,这里我们介绍下map的用法,实际很多场景都会用到
1 #include "stdafx.h" 2 #include "string.h" 3 #include <array> 4 #include <vector> 5 #include <map> 6 7 int _tmain(int argc, _TCHAR* argv[]) 8 { 9 typedef struct 10 { 11 int iRow; 12 int iColomn; 13 }ST_DATA, *PST_DATA; 14 15 std::array<PST_DATA, 4> arData; 16 std::map<int, PST_DATA> mapInfo; 17 18 //这样设计只是为了重温一下前面的内容 19 for (auto & atData : arData) 20 { 21 static int s_iNum = 0; 22 atData = new ST_DATA; 23 24 atData->iRow = ++s_iNum; 25 atData->iColomn = s_iNum; 26 } 27 28 //=============================================================================数据插入 29 //方式1: std::pair 30 mapInfo.insert(std::pair<int, PST_DATA>(3, arData.at(0))); 31 32 //方式2: std::value_type 推荐使用 33 mapInfo.insert(std::map<int, PST_DATA>::value_type(5, arData.at(1))); 34 35 //方式3:[ ] 36 mapInfo[7] = arData.at(2); 37 mapInfo[9] = arData.at(3); 38 39 for (auto atElement : mapInfo) 40 { 41 printf("map element: first: %d , second: irow: %d icolumn: %d\n", atElement.first, atElement.second->iRow, atElement.second->iColomn); 42 } 43 44 //查找元素 45 auto atMapElement = mapInfo.find(5); 46 if (atMapElement != mapInfo.end()) { printf("map find: %d %d\n", atMapElement->second->iRow, atMapElement->second->iColomn); } 47 48 //清空元素 49 for (auto & atElement : mapInfo) 50 { 51 if (nullptr != atElement.second) 52 { 53 delete atElement.second; 54 atElement.second = nullptr; 55 } 56 } 57 mapInfo.erase(mapInfo.begin(), mapInfo.end()); 58 59 return 0; 60 }
-
- 除了上述比较常见的容器外,STL还有很容器类型,如果对STL有兴趣,可以看下《Effective STL》学习一下,以前在项目也写过linux带超时的消息队列控制模板,还有与容器结合使用的一些算法,有兴趣的可以研究下,下面列出其他相关容器的一些特性。
容器名称 | 特性说明 | 包含头文件 |
---|---|---|
顺序容器 | ||
array | 固定数组。vector的底层即为array数组,它保存了一个以严格顺序排列的特定数量的元素 | <array> |
vector | 可变长数组。相当于数组,可动态构建,支持随机访问,使用inset插入元素,erase删除元素 | <vector> |
list | 双向循环链表。使用起来很高效,对于任意位置的插入和删除都很快,在操作过后,以后指针、迭代器、引用都不会失效 | <list> |
forward_list | 单向链表。只支持单向访问,在链表的任何位置进行插入/删除操作都非常快 | <forward_list> |
deque | 双端队列。支持头插、删,尾插、删,随机访问较vector容器来说慢,但对于首尾的数据操作比较方便 | <deque> |
关联式容器 | ||
set/multiset | 集合/多重集合。对于set,在使用insert插入元素时,已插入过的元素不可重复插入,这正好符合了集合的互异性,在插入完成显示后,会默认按照升序进行排序,对于multiset,可插入多个重复的元素 | <set> |
map/multimap | 映射/多重映射。二者均为二元关联容器(在构造时需要写两个参数类型,前者对key值,后者对应value值),multimap允许插入相同的key值,因此,multimap不支持下标运行符。 | <map> |
容器适配器 | ||
stack | 堆栈。其原理是先进后出(FILO),其底层容器可以是任何标准的容器适配器,默认为deque双端队列 | <stack> |
queue | 队列。其原理是先进先出(FIFO),只有队头和队尾可以被访问,故不可有遍历行为,默认也为deque双端队列 | <queue> |
pirority_queue | 优先队列。它的第一个元素总是它所包含的元素中优先级最高的,就像数据结构里的堆,会默认形成大堆,还可以使用仿函数来控制生成大根堆还是生成小根堆,若没定义,默认使用vector容器 | <queue> |
(3)字符串
以前有专门写过字符串的前世今生,这里就不再赘述了,有兴趣可以参考下windows字符串,当然不只这一点点内容,掌握了基础的知识,我们也就能更好地处理与之相关的问题,同时也总结了下关于文件/文件夹的一些操作,大概我自己用到的就这些内容了。
(4)结构体
-
- 结构体的基础信息
1 #include "stdafx.h" 2 #include "string.h" 3 #include <array> 4 #include <string> 5 6 int _tmain(int argc, _TCHAR* argv[]) 7 { 8 typedef struct 9 { 10 char cData; 11 std::string strData; 12 }ST_BASE, *PST_BASE; 13 14 //申明 15 ST_BASE stBase; 16 memset(&stBase, sizeof(stBase), 0);//确保数据安全 17 18 //定义 19 ST_BASE stBaseDefine = { 0 }; 20 21 //赋值 22 stBase.cData = 1; 23 stBase.strData = "abcdef"; 24 printf("struct element:%d %s\n", stBase.cData, stBase.strData.c_str()); 25 26 return 0; 27 }
-
- 内存对齐:C++的内存对齐机制的目的是让CPU能够在尽可能短的时间内获取到数据,从而提高整体性能。假如不采取内存对齐机制,则访问数据时还需要对成员变量的基本类型进行判断从而计算出正确的偏移量。而内存对齐系数则以变量类型的大小决定,前面我们提到过变量在内存中所占大小,所以内存对齐系数为“1,2,4,8,16”,而vs编译器默认的对齐系数为4字节对齐,可以使用#pragma pack(n)来重新指定大小。那这里为什么要提到这个概念?主要在上,下位机进行数据传输时,由于编译系统的不同默认的内存对齐方式也不同导致在不同系统下用结构体获取数据时可能出现偏差,但现在我们就知道如何处理这种情况了
1 #include "stdafx.h" 2 #include "string.h" 3 #include <array> 4 #include <string> 5 6 int _tmain(int argc, _TCHAR* argv[]) 7 { 8 typedef struct 9 { 10 short sData; 11 int iData; 12 }ST_BASE, *PST_BASE; 13 14 #pragma pack(1) 15 typedef struct 16 { 17 short sData; 18 int iData; 19 }ST_MEM, *PST_MEM; 20 #pragma pack() 21 22 ST_BASE stBase = { 0 }; 23 printf("struct base size: %d\n", sizeof(stBase)); // 8 24 25 ST_MEM stMem = { 0 }; 26 printf("struct mem size: %d\n", sizeof(stMem)); // 6 27 28 return 0; 29 }
-
- 结构体数组和结构体指针:我们需要注意的是结构体指针+1在内存上偏移量实际上为一个结构体大小
1 #include "stdafx.h" 2 #include "string.h" 3 #include <array> 4 #include <string> 5 6 int _tmain(int argc, _TCHAR* argv[]) 7 { 8 typedef struct 9 { 10 short sData; 11 int iData; 12 }ST_BASE, *PST_BASE; 13 14 std::array<ST_BASE, 6> arstBase = { 0 }; 15 //结构体指针偏移:atBase + 1在内存上的实际偏移量:是一个结构体大小 16 for (auto atBase = arstBase.begin(); atBase != arstBase.end(); atBase++) 17 { 18 static int s_iData = 0; 19 atBase->sData = ++s_iData; 20 atBase->iData = s_iData; 21 } 22 23 for (auto atBase : arstBase) { printf("struct element: %d %d\n", atBase.sData, atBase.iData); } 24 25 return 0; 26 }
-
- 结构体的位字段:一般在底层代码中会被用到的比较多,还有解析某些特定的协议数据时,所以我们还是有必要了解一下,特别是写固件或是对传输接口性能方向有要求时,都可以用来自定义协议
1 #include "stdafx.h" 2 #include <Windows.h> 3 4 int _tmain(int argc, _TCHAR* argv[]) 5 { 6 typedef struct 7 { 8 BYTE byType : 1; 9 BYTE byCheck : 1; 10 BYTE bySum : 2; 11 BYTE bySend : 4; 12 }ST_BASE, *PST_BASE; 13 14 ST_BASE stBase = { 0 }; 15 printf("struct base size: %d\n", sizeof (stBase)); // 1 16 17 return 0; 18 }
-
- 联合体:也叫共用体,其大小是联合体中所占内存最大的元素的长度。也是一般在底层或特定环境的代码中会用的比较多,主要目的是为了节省内存空间。
1 #include "stdafx.h" 2 #include <Windows.h> 3 4 int _tmain(int argc, _TCHAR* argv[]) 5 { 6 typedef union 7 { 8 char cType; 9 short sCheckSum; 10 int iData; 11 }UN_BASE, *PUN_BASE; 12 13 UN_BASE stBase = { 0 }; 14 printf("struct base size: %d\n", sizeof (stBase)); // 4 15 16 return 0; 17 }
-
- 枚举:用来管理混乱的宏常量有很好的效果,也可用在类的作用域中,更多地用在函数传参或条件变量表达式中
1 #include "stdafx.h" 2 #include <Windows.h> 3 4 int _tmain(int argc, _TCHAR* argv[]) 5 { 6 typedef enum 7 { 8 EN_BASE_RESET = 1, 9 EN_BASE_SEND, 10 EN_BASE_RECV, 11 EN_BASE_END, 12 }EN_BASE, *PEN_BASE; 13 14 int iParam = 0; 15 auto atSet = [](int & iPrm, EN_BASE enBase) { iPrm = enBase; }; 16 17 atSet(iParam, EN_BASE_RECV); 18 printf("iParam: %d\n", iParam); 19 20 return 0; 21 }
(5)指针
-
- 指针:我们且在栈上初始化一个指针,栈空间就会为指针分配一个32位内存空间,并在此内存上存储指针所指向的内存地址。
1 #include "stdafx.h" 2 3 int _tmain(int argc, _TCHAR* argv[]) 4 { 5 int iConst = 5; 6 int* piPoint = &iConst; 7 8 printf("point mem addr: %p\n", &piPoint);//栈上分配给指针的内存地址 9 printf("point save addr: %p\n", piPoint);//存储指针所指向的内存地址 10 printf("point get save addr value: %d\n", *piPoint);//获取指针所指向的地址存储的值 11 12 return 0; 13 }
-
- 指针的初始化:指针在我们项目中会经常用到,用的好,它就是一把利器;反之,将极大地提高我们的维护成本。下面我们就来说说为什么使用:char* pcInit = nullptr来初始化指针。
1 #include "stdafx.h" 2 3 void FnCall(void* pvPrm) 4 { 5 printf("FnCall point: %p\n", pvPrm); 6 } 7 8 void FnCall(int iPrm) 9 { 10 printf("FnCall int: %d\n", iPrm); 11 } 12 13 int _tmain(int argc, _TCHAR* argv[]) 14 { 15 FnCall(NULL);//NULL是我们认为的空指针,所以理论上应该调用第一个函数;可实际调用的是第二个函数 16 FnCall(nullptr);//nullptr是空指针,调用第一个函数 17 18 return 0; 19 }
-
- new/delete:对比C的malloc/free,new内置了sizeof、类型转换和类型安全检查功能,且new/delete处理类对象更便捷,下面我们也来说说使用new/delete需要注意的点
1 #include "stdafx.h" 2 #include <stdlib.h> 3 4 int _tmain(int argc, _TCHAR* argv[]) 5 { 6 //C语言 7 char* pcMalloc = (char*)malloc(sizeof(char) * 10); 8 if (nullptr != pcMalloc) { /*doing ... ...*/ } 9 if (nullptr != pcMalloc) 10 { 11 free(pcMalloc); 12 pcMalloc = nullptr; 13 } 14 15 //C++ 16 char* pcNew = new char[10]; 17 {/*doing ... ...*/} //这里不需要再做检测 18 for (int iNum = 0; iNum < 10; iNum++) { pcNew[iNum] = iNum; } 19 20 if (nullptr != pcNew) 21 { 22 //这里的delete与delete []没有区别,对于除类对象数组外的变量两种处理没有区别 23 delete pcNew; 24 //delete[]pcNew; 25 pcNew = nullptr;//需要初始化指针为空 26 } 27 28 29 //类测试 30 class CTest 31 { 32 public: 33 CTest() = default; 34 ~CTest() { printf("~CTest call\n"); } 35 }; 36 37 CTest *pobjTest = new CTest[10]; 38 39 //delete pobjTest;//这里析构函数只调用一次且抛出异常 40 delete []pobjTest;//析构被调用10次 41 42 return 0; 43 }
-
- 智能指针:这里暂时对智能指针进行些简单说明,确实用的不多,不也好为人师,下面只述说它的成因及一般出现的使用场景 。
- 为什么会出现智能指针?前面我们提到过堆上的内存是由程序员自己来维护,而一个大的项目往往由很多程序员来共同编写,可能我们本着"谁申请谁释放"的原则,但实际情况是在项目后期,内存泄漏的问题给调试和维护带来了很大困扰。同时,像JAVA这种语言提出了系统回收机制,即不管是不是由堆上申请的内存,都由系统负责回收,于是C++工作人员提出智能指针来解决内存泄漏的问题;
- 四种智能指针类型auto_ptr(已废除), nique_ptr, shared_ptr, week_ptr,我们比较常用的原生指针和unique_ptr,建议要在堆上申请内存时使用unique_ptr来代替,从而消除delete的操作。、
1 #include "stdafx.h" 2 #include <memory> 3 #include <array> 4 5 #define ARRAY_NUM (10) 6 7 //如果明确参数为数组时,请使用下面的格式,代码更清晰 8 void FnCall(int aiArray[], int iArrayNum) 9 { 10 for (int iCount = 0; iCount < iArrayNum; iCount++) 11 { 12 aiArray[iCount] = iCount; 13 } 14 } 15 16 int _tmain(int argc, _TCHAR* argv[]) 17 { 18 //智能指针的用法在这里就不作说明 19 //C++11 20 std::unique_ptr<int[]> aiArray{ new int[ARRAY_NUM]{0} };//定义及初始化 21 22 FnCall(aiArray.get(), ARRAY_NUM); 23 for (int iNum = 0; iNum < ARRAY_NUM; iNum++) 24 { 25 printf("aiArray element: %d \n", aiArray[iNum]); 26 } 27 28 //C++14 29 std::unique_ptr<int[]> aiArrayS = std::make_unique<int[]>(ARRAY_NUM);//初始化为0 30 31 FnCall(aiArrayS.get(), ARRAY_NUM); 32 for (int iNum = 0; iNum < ARRAY_NUM; iNum++) 33 { 34 printf("aiArray element: %d \n", aiArrayS[iNum]); 35 } 36 37 return 0; 38 }
5. 关系表达式
(1)循环表达式
-
-
for:我们必须要弄清楚for的执行逻辑,我们才能更好地使用它,同时随着C++的发展,也衍生出了for each和C++11里添加的算法表达式std:for_each,下面我们就结合代码来看它们之间的区别
-
图片来源:http://c.biancheng.net/view/1372.html
1 #include "stdafx.h" 2 #include <array> 3 4 int _tmain(int argc, _TCHAR* argv[]) 5 { 6 7 char acArray[6] = {0}; 8 9 //传统方式 10 for (int iNum = 0; iNum < sizeof (acArray) / sizeof(acArray[0]); iNum++) 11 { 12 acArray[iNum] = iNum; 13 } 14 15 //for earch遍历: 对比传统方式,表达式更简洁 16 for each(auto atArray in acArray) 17 { 18 printf("acArray element: %d\n", atArray); 19 } 20 21 printf("==========================================================\n"); 22 std::array<int, 6> arArray = { 0 }; 23 24 //容器遍历 25 int iTemp = 0; 26 for (auto & atArray : arArray) 27 { 28 atArray = iTemp++; 29 } 30 31 //C++11算法遍历:这里还能扩展,eg.传入this指针就可以操作类成员变量,后面讲类的时候再研究 32 std::for_each(arArray.begin(), arArray.end(), [=](int iData) 33 { 34 printf("arArray element: %d\n", iData); 35 }); 36 37 38 return 0; 39 }
-
- while/do while:在主线程里我们更多地使用for循环(一般循环次数已知),而while循环更多地出现子线程中用来接收或发送数据,下面代码我们也将在线程中来实现while循环
1 #include "stdafx.h" 2 #include <future> 3 4 int _tmain(int argc, _TCHAR* argv[]) 5 { 6 //这里只要知道是一个线程就好了,后面会讲到async这个概念 7 auto atThread = std::async(std::launch::async, []() 8 { 9 int iNum = 0; 10 while (iNum < 9999999) 11 { 12 iNum++; 13 } 14 printf("thread exit\n"); 15 }); 16 17 atThread.wait();//等待线程退出 18 printf("wait thread exit\n"); 19 20 printf("============================================================\n"); 21 //do while 22 23 do 24 { 25 printf("do while execute\n"); 26 } while (false);//先执行一次,条件表达式成立,则继续执行;否则,将退出循环 27 28 //实现上述功能 29 auto atDoWhileThread = std::async(std::launch::async, []() 30 { 31 int iNum = 0; 32 do 33 { 34 if (iNum++ == 999999999) break; 35 36 } while (true); 37 printf("do while thread exit\n"); 38 }); 39 40 atDoWhileThread.wait(); 41 printf("wait do while thread exit\n"); 42 43 return 0; 44 }
-
-
continue/break:在for/while循环中我们根据需求经常会用到continue/break,那么具体的使用方法我们结合来进行说明
-
1 #include "stdafx.h" 2 #include <array> 3 4 int _tmain(int argc, _TCHAR* argv[]) 5 { 6 std::array<int, 6> arArray = {1, 2, 3, 4, 5, 6}; 7 8 for (auto atData : arArray) 9 { 10 if (4 == atData) 11 continue; //如果条件表达式成立,则不向下执行,返回for()的条件表达式继续执行 12 13 printf("atData: %d\n", atData); //打印:1,2,3,5,6 14 } 15 printf("===========================================\n"); 16 17 for (auto atData : arArray) 18 { 19 if (4 == atData) 20 break;//如果条件表达式成立,则将直接跳出for循环 21 22 printf("atData: %d\n", atData);//打印:1,2,3 23 } 24 return 0; 25 }
(2)分支语句
-
- if else:语法逻辑比较简单,我们就以代码的形式进行说明
1 #include "stdafx.h" 2 3 int _tmain(int argc, _TCHAR* argv[]) 4 { 5 //if else 6 int iNum = 10; 7 8 if (5 > iNum) 9 { 10 printf("5 > iNum\n"); 11 } 12 else if (10 > iNum) 13 { 14 printf("10 > iNum\n"); 15 } 16 else if (20 > iNum) 17 { 18 printf("20 > iNum\n");//满足条件,执行完跳出分支语句 19 } 20 else if (10 == iNum) 21 { 22 printf("10 == iNum\n");//满足条件,但不执行 23 } 24 else 25 { 26 printf("else\n"); 27 } 28 29 return 0; 30 }
-
- switch/case:语法逻辑与if else差不多,不过结构上有差别,而且需switch() {case 常量: {... break;}}配合使用,而且一般都出现在for/while循环中,下面我们还是以代码的形式进行说明
1 #include "stdafx.h" 2 #include <array> 3 4 int _tmain(int argc, _TCHAR* argv[]) 5 { 6 std::array<int, 6> arArray = {1, 2, 3, 4, 5, 6}; 7 8 for (auto atData : arArray) 9 { 10 switch (atData) 11 { 12 case 1://满足条件才执行{}内容 13 { 14 printf("enter 1\n"); 15 break;//break用来跳出switch语句(不会跳出for循环),如果不加break,则执行完继续向下判断 16 } 17 case 2: 18 printf("enter 2\n"); 19 break; 20 case 3: 21 printf("enter 3\n"); 22 break; 23 case 4: 24 printf("enter 4\n"); 25 break; 26 case 5: 27 printf("enter 5\n"); 28 break; 29 default: 30 break; 31 } 32 33 //在这里添加break,才会跳出for循环,需注意 34 } 35 36 return 0; 37 }
6. 函数
(1)函数基础信息
我们学习C++接触最多的应该都是跟函数打交道,那为什么要使用函数呢?可能在项目中给我带来最直观的感受是:模块化和代码复用。想要用好函数,我们就有必要掌握它的相关知识点,下面我们先聊聊函数的模型及函数的申明,定义和调用。
1 #include "stdafx.h" 2 3 //这里暂时不用类来进行设计,后面讲到类,我们再使用 4 5 //函数的申明 6 double FnCall(double dArg, double dPrm); 7 8 //函数定义 9 double FnCall(double dArg, double dPrm) 10 { 11 return dArg + dPrm; 12 } 13 14 int _tmain(int argc, _TCHAR* argv[]) 15 { 16 //函数的调用 17 double dRet = FnCall(3.0, 5.0); 18 printf("dRet: %lf\n", dRet); 19 20 return 0; 21 }
(2)函数的参数
1 #include "stdafx.h" 2 3 double FnCall(double dArg, double dPrm) 4 { 5 return dArg + dPrm; 6 } 7 8 //隐式转换测试函数 9 int Sum(int iArg, int dPrm) 10 { 11 return iArg + dPrm; 12 } 13 14 //explicit 限制隐式转换, 不过只作用于带一个参数的构造函数, 后面讲到类的时候我们再重点讲它 15 class CExplicit 16 { 17 public: 18 CExplicit() = default; 19 explicit CExplicit(int iPrm) {}; 20 ~CExplicit() {}; 21 }; 22 23 24 int _tmain(int argc, _TCHAR* argv[]) 25 { 26 //隐式转换: 调用函数时,函数内部参数转换成double类型 27 double dRet = FnCall(3, 5); 28 printf("dRet: %lf\n", dRet); 29 30 //隐式转换失败 31 //int iData = 4; 32 //double dReturn = FnCall(&iData, 5);//编译器报错,也就是说不是所有类型都能进行隐式转换 33 34 //隐式转换造成精度丢失 35 int iRet = Sum(3.4, 5.1);//iRet = 8 36 37 //使用explicit来取消隐式转换 38 //CExplicit objExplicit = 4;//编译器报错 39 40 //显式转换 41 int iData = 4; 42 double dTemp = (double)iData; 43 44 return 0; 45 }
-
- 参数传递:我们常见的参数传递有值传递,指针传递,引用传递及C++11新增了右值引用的传递,下面我们结合代码总结一下这几种传递方式:
1 #include "stdafx.h" 2 #include <array> 3 4 void FnValue(int iPrm) 5 { 6 iPrm = 10; 7 printf("FnValue iPrm addr: %p\n", &iPrm);//#00effba4 8 } 9 10 void FnPoint(int* piPrm, int iSize) 11 { 12 if (nullptr == piPrm) return; 13 14 for (int iNum = 0; iNum < iSize; iNum++) 15 { 16 piPrm[iNum] = iNum; 17 } 18 printf("piPrm addr: %p &piPrm addr: %p\n", piPrm, &piPrm);//#004FF718 #004FF610 19 } 20 21 void FnQuote(int & iPrm) 22 { 23 iPrm = 20; 24 printf("iPrm addr: %p &piPrm:%p \n", &iPrm);// #001AF878 25 } 26 27 void FnQuoteRight(int && iPrm) 28 { 29 iPrm = 20; 30 printf("iPrm addr: %p \n", &iPrm); 31 } 32 33 int _tmain(int argc, _TCHAR* argv[]) 34 { 35 int iData = 5; 36 int aiArray[4] = { 0 }; 37 38 //值传递: 当调用函数时,参数传入实际是拷贝了一份实参的副本, 39 //从例子可以看出:形参与实参存放的地址是不同的,对形参进行赋值,并不会影响实参的值 40 FnValue(iData); 41 printf("iData value:%d iData addr:%p\n", iData, &iData);//5, #00effc90 42 43 printf("====================================================\n"); 44 //指针传递:本质也是值传递,传参时拷贝了一份指针的内容(指针存储的实参地址)的值,而栈上会开辟一个4字节的地址存放指针本身 45 //从例子可以看出:存放指针的内存地址是不同的,但piArray,piPrm所存放的内存地址是一致的 46 int* piArray = aiArray; 47 //为什么不直接使用数组名传参,前面讲数组时有提到过数组名与数组指针的区别,这里使用数组指针只是方便解释 48 FnPoint(piArray, sizeof(aiArray) / sizeof (aiArray[0])); 49 for each(auto atData in aiArray) { printf("aiArray element: %d\n", atData); }//0, 1, 2, 3 50 printf("aiArray addr:%p &aiArry addr:%p\n", piArray, &piArray);//#004FF718 #004FF70C 51 52 printf("=======================================================\n"); 53 //引用传递: 这里我们可以当成是指针传递,这样就很好理解了?为什么这么说呢 54 //函数在被调用时,栈上会临时开辟一个4字节的空间来存放实参变量的地址,然后通过间接寻址的方式操作实参 55 int iQuote = 6; 56 FnQuote(iQuote); 57 printf("iQuote value:%d iQuote addr:%p\n", iQuote, &iQuote);//20 #001AF878 58 59 printf("=======================================================\n"); 60 //右值引用传递:"&"是给左值具名,"&&"给右值具名,与引用传递的方式一致,后面C++11里面会讲到这个概念 61 int iQuoteLeft = 8;//左值 62 int &&iQuoteRight = 8;//右值 63 FnQuoteRight(std::move(iQuoteRight));//移动语义 64 printf("iQuoteRight value: %d iQuoteRight addr:%p \n", iQuoteRight, &iQuoteRight); 65 66 return 0; 67 }
-
- 缺省参数:缺省参数为我们写代码提供了更便捷的方式。比如在设计类时,可以减少要定义的析构函数,方法及方法要重载的数量。使用比较简单,下面就结合代码说明一下
1 #include "stdafx.h" 2 #include <array> 3 4 int FnValue(int iArg = 4, int iPrm = 5, int iTmep = 10); 5 6 int FnValue(int iArg, int iPrm, int iTemp) 7 { 8 return (iArg + iPrm - iTemp); 9 } 10 11 int _tmain(int argc, _TCHAR* argv[]) 12 { 13 printf("Set Paramer num 0: %d \n", FnValue()); //-1 14 printf("Set Paramer num 1: %d \n", FnValue(6)); //1 15 printf("Set Paramer num 2: %d \n", FnValue(6, 7)); //3 16 printf("Set Paramer num 3: %d \n", FnValue(6, 7, 8));//5 17 18 return 0; 19 }
(3)函数指针
-
- 前面我们学习程序的内存分布时了解到函数体是存放在“代码段”区,主程序在运行时碰到函数的调用,就需要跳转到函数的入口地址去执行函数代码,执行完毕再跳转回主程序。函数指针就是指向函数的入口地址,有了它,我们就可以在需要的地方随时调用函数了。
- 函数指针与函数名:函数名不等同于指针,对函数名取地址会发现与函数名指向的地址一样都是函数的入口地址,但作为参数传递时,函数名会转换成指针!可以温习下数组指针与数组名,是不是很像?
- 函数指针的应用很广,像回调函数指针,类对象指针,虚表和指针等,我们慢慢学,慢慢揭秘,不过不要把函数指针设计的过于复杂,像大学考试那种基本不会用到,设计代码本身就应该简单易读。
1 #include "stdafx.h" 2 3 int FnCall(int iArg) 4 { 5 printf("iArg: %d\n", iArg); 6 return iArg; 7 } 8 9 int _tmain(int argc, _TCHAR* argv[]) 10 { 11 //方式一 12 int(*pfnFnCall)(int) = FnCall; 13 pfnFnCall(2); 14 15 //方式二 16 typedef int (*FNCALL)(int); 17 FNCALL pfnFnCallTypedef = FnCall; 18 pfnFnCallTypedef(4); 19 20 //方式三 21 using FNCALLUS = int(*)(int); 22 FNCALLUS pfnFnCallUs = FnCall; 23 pfnFnCallUs(8); 24 25 //函数指针与函数名 26 printf("pfnCall: %p FnCall: %p &FnCall: %p\n", pfnFnCall, FnCall, &FnCall);//全部为#00E21163 27 28 return 0; 29 }
(4)const修饰
1 #include "stdafx.h" 2 3 //define 4 #define TEMP (4) 5 #define SUM TEMP + TEMP 6 #define DIFF SUM - SUM //所以代码规范很重要,()能解决 7 8 //const 9 const int g_iSum = TEMP + TEMP; 10 const int g_iDiff = g_iSum - g_iSum; 11 12 int _tmain(int argc, _TCHAR* argv[]) 13 { 14 /*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*define与const=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=**/ 15 //define与const:DIFF值与预期的不符?我们展开一下:#define DIFF TEMP + TEMP - TEMP + TEMP 16 printf("SUM: %d DIFF:%d \n", SUM, DIFF);//8 8 17 printf("g_iSum:%d g_iDiff:%d\n", g_iSum, g_iDiff);//8 0 18 19 /*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*const修饰指针=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=**/ 20 //指针:const在指针左,指针指向的地址的内容为不可变量;const在指针右边,指针所指向的地址为不可变量 21 int iData = 4; 22 int iTemp = 10; 23 const int* piLeft = &iData; 24 //*piLeft = 5; 编译器报错 25 int* const piRight = &iData; 26 *piRight = 7; 27 printf("iData:%d\n", iData);//7 28 //piRight = &iTemp;编译器报错 29 30 /*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*const修饰函数参数=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=**/ 31 //const 参数传递:为了方便观看,这里用lamdba表达式来替代函数 32 //内置类型 33 auto atConstant = [](const int iData) 34 { 35 //iData = 10; //编译器报错 36 }; 37 38 //指针:指针所指向的地址的内容不可更改,所以在只是读取数据,不作修改操作时,请尽量使用const 39 auto atConstPoint = [](const int* piData) 40 { 41 //++*piData;编译器报错 42 }; 43 44 //引用:是不是觉得有点多此一举啦,用"&"本身就想着作为输出参数或是减小开销? 45 //是的,一般情况用不到const,只有在查询类作为函数参数时,才用到它 46 auto atConstQuote = [](const int & iData) 47 { 48 //iData = 4;//编译报错 49 }; 50 51 /*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*const修饰成员函数=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=**/ 52 //const修饰成员函数,在函数内不能修改成员变量的值 53 class CTemp 54 { 55 public: 56 void FnCall(int iPrm) const 57 { 58 //m_iData = iPrm;编译器报错 59 } 60 public: 61 CTemp() :m_iData(0) {}; 62 ~CTemp() {} 63 private: 64 int m_iData; 65 }; 66 67 return 0; 68 }
(5)递归函数
-
- 递归函数实质是一直重复地调用自己,但须设置条件以满足递归出口,否则,程序循环往复。
- 结合前面的知识点,我们知道调用函数时,栈是需要临时开辟空间以存放临时变量,返回值等,而递归在满足出口条件之前,实际上一直在做重复压栈,这就可能引发一些问题?当临时变量过大或递归深度太高就可能引起“栈溢出”,所以在设计递归时我们还得注意此点(主要体现在下位机中,栈空间分配不够)
- 递归最大的好处就是使用程序结构清晰易懂,在底层算法中应用特别广泛。
1 #include "stdafx.h" 2 3 void FnRecur(int iPrm) 4 { 5 if (0 < iPrm) { FnRecur(iPrm - 1); } 6 7 printf("iPrm:%d\n", iPrm); 8 } 9 10 int Sum(int iAdd) 11 { 12 if (1 >= iAdd) 13 return iAdd;//递归出口 14 else 15 return (iAdd + Sum(iAdd - 1)); 16 } 17 18 template <typename T> 19 T AddSum(T t) 20 { 21 return t; 22 } 23 24 template<typename T, typename... U> 25 T AddSum(T first, U... end) 26 { 27 return (first + AddSum<T>(end...)); 28 } 29 30 int _tmain(int argc, _TCHAR* argv[]) 31 { 32 //递归:再温习下程序执行,函数体是放在代码段的,主程序在调用函数时,跳转函数的入口地址执行语句,此时 33 //栈上分配临时变量,参数,返回值的内存空间,函数执行完后再由栈释放这一部分内存 34 //递归函数实际上在出去之前一直重复压栈的过程,而我们知道栈是"先进后出",所以下面例子的打印顺序就清楚了 35 FnRecur(5); //打印顺序:0 1 2 3 4 5 36 37 //计算1+n 38 printf("iSum:1+...10: %d\n", Sum(10)); //55 39 printf("iSum:1+...100: %d\n", Sum(100));//5050 40 41 //计算器"+"号功能:模板参数,下面马上要讲到了,这里只作展示 42 printf("AddSum:1+2: %d\n", AddSum(1, 2)); //3 43 printf("AddSum:1+2+3: %d\n", AddSum(1, 2, 3)); //6 44 printf("AddSum:1+2+3+4+5: %d\n", AddSum(1, 2, 3, 4, 5));//15 45 46 return 0; 47 }
(6)回调函数
回调函数本质:就是把函数指针作为函数参数。像MFC,动态库的显式调用等很多场景都会用到,下面用几个例子简单说明一下,关于function与bind在这里暂时不说明 ,代码展示如下:
1 #include "stdafx.h" 2 3 using CALLBACK = void(*)(int); 4 5 void CallBack(int iArg) 6 { 7 printf("callback iarg: %d\n", iArg); //5 8 } 9 10 void FnCall(CALLBACK pfnCallBack, int iArg) 11 { 12 if (nullptr == pfnCallBack) return; 13 14 pfnCallBack(iArg); 15 } 16 17 int _tmain(int argc, _TCHAR* argv[]) 18 { 19 //最简单的回调 20 FnCall(CallBack, 5); 21 22 23 return 0; 24 }
1 #include "stdafx.h" 2 #include <process.h> 3 #include <Windows.h> 4 #include <future> 5 6 class CTest 7 { 8 public: 9 void InitThread() 10 { 11 m_hThread = (HANDLE)_beginthreadex(nullptr, 0, DoThread, this, 0, 0); 12 } 13 14 static unsigned _stdcall DoThread(void* pvArg) 15 { 16 CTest* pobjTest = static_cast<CTest*>(pvArg); 17 pobjTest->RunThread(); 18 19 return 0; 20 } 21 22 void RunThread() 23 { 24 printf("thread run=============\n"); 25 } 26 27 void UninitThread() 28 { 29 if (nullptr != m_hThread) CloseHandle(m_hThread); 30 } 31 public: 32 CTest():m_hThread(nullptr){ InitThread(); }; 33 ~CTest() { UninitThread(); }; 34 35 private: 36 HANDLE m_hThread; 37 }; 38 39 40 int _tmain(int argc, _TCHAR* argv[]) 41 { 42 //这是项目中经常要打交道的,这里就使用了回调作为线程函数 43 CTest objTest; 44 45 Sleep(1000);//等待下线程退出 46 printf("============================================================\n"); 47 48 //C++11线程 49 auto atThread = std::async(std::launch::async, []() 50 { 51 std::this_thread::sleep_for(std::chrono::seconds(5)); 52 printf("atThread Run=================\n"); 53 }); 54 //主线程也在运行 55 printf("main()=================\n"); 56 atThread.wait();//等待线程退出 57 58 return 0; 59 }
1 #include "stdafx.h" 2 #include <process.h> 3 #include <Windows.h> 4 #include <future> 5 6 //将回调设计成接口类给用户自定义 7 class ICallBack 8 { 9 public: 10 virtual void CallBack() = 0; 11 12 public: 13 ICallBack() = default; 14 virtual ~ICallBack() = default; 15 }; 16 17 //业务类 18 class CFnCall 19 { 20 public: 21 void SetCallBack(ICallBack* pobjCallBack) 22 { 23 m_pobjCallBack = pobjCallBack; 24 } 25 26 ICallBack* GetCallBack() const 27 { 28 return m_pobjCallBack; 29 } 30 31 void FnCall() 32 { 33 //doing something.... 34 if (1/*获取结果*/ && (nullptr != m_pobjCallBack)) m_pobjCallBack->CallBack(); 35 } 36 public: 37 CFnCall() :m_pobjCallBack(nullptr){}; 38 ~CFnCall() {}; 39 40 private: 41 ICallBack* m_pobjCallBack; 42 }; 43 44 //用户 45 class CUser : public ICallBack 46 { 47 public: 48 virtual void CallBack() override 49 { 50 printf("user custom function\n"); 51 } 52 53 public: 54 CUser() = default; 55 virtual ~CUser() = default; 56 }; 57 58 59 int _tmain(int argc, _TCHAR* argv[]) 60 { 61 //C++面向接口编程 62 CUser objUser; 63 CFnCall objFnCall; 64 65 objFnCall.SetCallBack(&objUser); 66 objFnCall.FnCall(); 67 68 return 0; 69 }
(7)内联函数
1 #include "stdafx.h" 2 3 inline void FnCall(int iArg) 4 { 5 printf("&iArg: %p\n", &iArg); //#0019F72C 6 } 7 8 int _tmain(int argc, _TCHAR* argv[]) 9 { 10 int iData = 10; 11 FnCall(iData); 12 13 //下面的例子两地址说明:内联函数执行时还是需要栈上开辟空间来存放参数,临时变量等,所以内联函数 14 //比普通函数多一个存放函数体的空间,这也是为什么内联函数的程序体不宜过大 15 printf("&iData: %p\n", &iData);//#0019F800 16 17 return 0; 18 }
(8)仿函数
这里提出这个概念,具体说明将放到“运算符重载”里进行详细说明。
(9)函数模板
-
- C++是支持函数重载的,重载的好处是能实现接口的统一,但一旦支持的类型过多时,代码量可想而知,而函数模板可以很好地简化代码量。下面我们就以例子进行说明:
1 #include "stdafx.h" 2 3 char Sum(char cArg, char cPrm) 4 { 5 return (cArg + cPrm); 6 } 7 8 int Sum(int iArg, int iPrm) 9 { 10 return (iArg + iPrm); 11 } 12 13 float Sum(float fArg, float fPrm) 14 { 15 return (fArg + fPrm); 16 } 17 18 double Sum(double dArg, double dPrm) 19 { 20 return (dArg + dPrm); 21 } 22 //... 23 24 //函数模板实现 25 template<typename T> 26 T AddSum(T t, T u) 27 { 28 return (t + u); 29 } 30 31 int _tmain(int argc, _TCHAR* argv[]) 32 { 33 //*******************函数重载实现 34 int iRet = Sum(-10, 20); 35 printf("iRet: %d \n", iRet); 36 37 //注意:3.1如不添加“f”,函数原型认定为double参数类型 38 float fRet = Sum(3.1f, 4.2f); 39 printf("fRet: %f \n", fRet); 40 41 double dRet = Sum(1.123, 2.123); 42 printf("lfRet: %lf \n", dRet); 43 44 char cRet = Sum(1i8, 2i8); 45 printf("iRet: %d \n", cRet); 46 47 printf("==========================================\n"); 48 //*********************函数模板实现 49 printf("AddSum: %d\n", AddSum(-10, 20)); 50 printf("AddSum: %f\n", AddSum(3.1f, 4.2f)); 51 printf("AddSum: %lf\n", AddSum(1.123, 2.123)); 52 printf("AddSum: %d\n", AddSum(1i8, 2i8)); 53 54 return 0; 55 }
-
- 函数模板的实例化
1 #include "stdafx.h" 2 3 template<typename T, typename U> 4 bool Compare(T t, U u) 5 { 6 return (t > u); 7 } 8 9 //显式实例化:当某个类型被频繁调用时,可以显式实例化 10 template bool Compare(double, double); //方式1 11 //template bool Compare<double>(double, double);//方式2 12 13 int _tmain(int argc, _TCHAR* argv[]) 14 { 15 16 //隐式实例化 17 bool bRet = Compare(5, 7); 18 19 //显式实例化调用 20 bool bRetDouble = Compare(6.0, 7.0); 21 22 return 0; 23 }
-
- 函数模板的特化(具体化)
1 #include "stdafx.h" 2 3 typedef struct 4 { 5 int iSeconds; 6 }ST_TEMP, *PST_TEMP; 7 8 //函数模板 9 template<typename T, typename U> 10 bool Compare(T & t, U & u) 11 { 12 return (t > u); 13 } 14 15 //全特化 16 template<> 17 bool Compare(ST_TEMP & stArg, ST_TEMP & stPrm) 18 { 19 return (stArg.iSeconds > stPrm.iSeconds); 20 } 21 22 //偏特化:编译器报错,C++不支持函数模板的偏特化,但支持类模板的偏特化 23 //template<> 24 //bool Compare(T & t, int iPrm) 25 //{ 26 // return (t > iPrm); 27 //} 28 29 int _tmain(int argc, _TCHAR* argv[]) 30 { 31 int iArg = 4; 32 int iPrm = 5; 33 34 bool bRet = Compare(iArg, iPrm);//false 35 //bool bRet = Compare(5, 6);//直接报错,后面讲类型推导会进行说明 36 37 ST_TEMP stArg = { 4 }; 38 ST_TEMP stPrm = { 2 }; 39 40 //全特化:结构体不能比较大小,需要进行特殊处理,全特化很好地解决了这个问题 41 bool bRetSt = Compare(stArg, stPrm);//true 42 43 return 0; 44 }
-
- 可变参数模板:对于库开发来说,可变参数模板就比较常见了,这是C++11提出的新特性,它对参数进行了高度泛化,可以表示0到任意个数,任意类型的参数。可变参数模板一般有三种参数包展开方式:一般展开,递归展开和逗号展开,一般我们会用到递归展开方式,下面我们结合代码说明:
1 #include "stdafx.h" 2 #include <iostream> 3 4 template<typename... T> 5 void GetPrmNum(T... t) 6 { 7 printf("count paramer num: %d\n", sizeof...(t));//sizeof出来的是参数个数 8 } 9 10 //==================================================== 11 template<typename T> 12 T Add(T t)//递归终止函数 13 { 14 return t; 15 } 16 17 template<typename T, typename... U> 18 T Add(T first, U... end)//参数展开包函数 19 { 20 return (first + Add<T>(end...));//按参数个数展开Add(1,2,3):1 + Add(2) + Add(3) 21 } 22 23 //==================================================== 24 template<typename T, typename... U> 25 void FnCall(T & pfn, U&&... args) 26 { 27 //只是为了展示有逗号展开的这种方式:生成有3个元素,且值为0的数组,其本身没有实质的意义 28 std::initializer_list<int> arArray = { (pfn(std::forward<U>(args)), 0)... }; 29 } 30 31 int _tmain(int argc, _TCHAR* argv[]) 32 { 33 //普通展开:由左至右顺序展开包 34 GetPrmNum(1); // 1 35 GetPrmNum(2, 4, 5); //3 36 37 printf("=======================================\n"); 38 //递归展开:需要一个参数包展开函数和一个递归终止函数,标准用法 39 printf("Add(1,2): %d\n", Add(1, 2)); //3 40 printf("Add(1,2): %d\n", Add(1, 2, 3));//6 41 42 printf("=======================================\n"); 43 //逗号展开:配合初始化列表使用展示,这里不作说明,后面了解C++11后应该就理解了 44 FnCall([](int iData){std::cout << iData << std::endl; }, 1, 3, 4); 45 }
-
- 模板参数的类型推导:我们前面了解了函数模板的执行过程 ,而生成一个函数实例前,我们需要对实参进行类型推导,而类型T&在编译器上推导出来仍是T&,也就意味编译器会对T取地址存放在栈上,而此时如果传入的实参为临时值时,对临时值取地址必然失败(临时值不存在内存域,直接存放在CPU寄存器),编译器报错,那我们就需要重写一个类型为T的函数用来调用临时值,然而这样设计,明显偏离了我们的初衷,为此,C++11提出了右值引用T&&的概念来同时包含临时值与T&。
1 #include "stdafx.h" 2 3 template<typename T> 4 T Sum(T arg, T prm)//类型不可知,取决于实参类型为左值或右值 5 { 6 return (arg + prm); 7 } 8 9 //& 10 template<typename T> 11 T SumAnd(T & arg, T & prm)//类型推导为左值,所以传入右值时报错 12 { 13 return (arg + prm); 14 } 15 16 //&&: 这是我们想要的函数模板 17 template<typename T> 18 T SumAndAnd(T && arg, T && prm)//类型不可知,取决于实参类型为左值或右值 19 { 20 return (arg + prm); 21 } 22 23 int _tmain(int argc, _TCHAR* argv[]) 24 { 25 //内置类型 26 int iArg = 4; 27 int iPrm = 6; 28 29 auto atRet = Sum(4, 6); 30 auto atRetVar = Sum(iArg, iPrm); 31 32 //&类型,一般当数据类型过大时,&可以节省栈上的内存开销, 33 //auto atAndRet = SumAnd(4, 6);//编译器报错 34 auto atAndRet = SumAnd(iArg, iPrm); 35 36 //&&类型 37 auto atAndAndRet = Sum(4, 6); 38 auto atAndAndRetVar = Sum(iArg, iPrm); 39 }
-
- 说明:函数模板和后面要讲到的类模板,是泛型编程的基础,高度泛化的代码给我们最直观的感受是,大量减少了代码量并实现了强大的功能,同时,也给我们带来了很多的困扰,比如调试起来比较麻烦,性能对比传统编码有所欠缺,甚至基础相对差一些的,看懂都比较费劲。那我们到底要不要学?首先,基础知识我们还是有必要学扎实,其次,取决于你当前的项目环境,公司代码要求等综合起来考量,如果是偏底层C++,对传输接口有性能要求,那尽量避免高度泛化的代码;如果你准备从事库开发,那么你就要学好它了。语言无关好坏,取决于我们如何用好它!
(10)函数总结
7. 类与对象
(1)类与对象基础
-
- C++的核心面向对象编程,这意味着我们写代码时主要要和它打交道,前面我们学了很多的数据类型(eg.比较基础的内置类型char,int等,也有复杂点的类型数组,结构体等),其实类我们可以认为是具备多态特性的数据类型,对象可以认为是类型变量,可能这样就相对比较好理解。C++编译器为了兼容C的struct,把struct按类进行解析,但是,我们还是希望不要用struct实现类功能,避免发生歧义。
- 刚开始学习类的时候,最直观的感受就是它的“封装”特性:将用户想要接口开放出去,对用户不关心的数据进行封装。这样做的好处就是避免类的数据被随意篡改,下面开始展示类的一些特性
1 class CBase 2 { 3 //类成员函数 4 public: 5 void FnCall() 6 { 7 printf("m_cBase: %d\n", m_cBase); 8 } 9 10 //构造与析构 11 public: 12 CBase() : m_iBase(10), m_cBase(5) {}; 13 virtual ~CBase() {}; 14 15 //派生类可以访问 16 protected: 17 int m_iBase; 18 19 //只在类作用域内使用 20 private: 21 char m_cBase; 22 }; 23 24 //=============================================================== 25 //派生类,用来作展示, 讲到派生类再展开 26 class CTemp : public CBase 27 { 28 public: 29 int GetBase() const 30 { 31 return m_iBase; 32 } 33 34 public: 35 CTemp() = default; 36 virtual ~CTemp() {}; 37 }; 38 39 int _tmain(int argc, _TCHAR* argv[]) 40 { 41 CBase objBase;//objBase为类对象 42 objBase.FnCall();//objBase只能访问public的成员 43 44 CTemp objTemp; 45 printf("CTmep::m_iBase: %d\n", objTemp.GetBase()); 46 }
-
-
类对象的创建与销毁:类对象的创建和销毁过程对于我们学好类还是很重要的,那么,在生成和销毁一个类对象时,编译器到底做了哪些事?
-
(2)构造函数
-
- 上文我们提到过“类对象的创建与销毁”,在我们不重写构造函数时,编译器会为我们生成几个构造函数,下面我们就用代码检验一下其特性
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 ~CBase() {}; 7 }; 8 9 //用户重载构造函数 10 class CUser 11 { 12 public: 13 int GetValue() const 14 { 15 return m_iData; 16 } 17 18 public: 19 CUser() :m_iData(0) 20 { 21 22 } 23 24 CUser(int iData) : m_iData(iData) 25 { 26 27 } 28 29 private: 30 int m_iData; 31 }; 32 33 int _tmain(int argc, _TCHAR* argv[]) 34 { 35 //编译器自动生成默认构造函数,拷贝构造函数,赋值构造函数 36 CBase objBase;//默认构造 37 printf("objBase : %d\n", sizeof(objBase)); //1 空对象占用1个字节的空间 38 CBase objCopy(objBase);//拷贝 39 CBase objSetValue = objBase; //赋值 40 41 //用户重载构造函数 42 CUser objUser; 43 printf("cobjUser m_iData: %d\n", objUser.GetValue()); //0 44 CUser objUserSet(10); 45 printf("cobjUserSet m_iData: %d\n", objUserSet.GetValue()); //10 46 }
-
- 拷贝构造函数:我觉得大家应该弱化这个所谓的浅拷贝与深拷贝的概念,搞清楚产生问题的原因才是关键。总结起来就是编译器生成的拷贝构造函数不能满足用户的所有需求,需要用户自己重写这个函数
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 CBase() 7 { 8 m_piArray = new int[10]; 9 } 10 11 ~CBase() 12 { 13 if (nullptr != m_piArray) 14 { 15 delete []m_piArray; 16 m_piArray = nullptr; 17 } 18 }; 19 20 private: 21 int* m_piArray; 22 }; 23 24 //将new放在非静态成员函数内=====================++++++++++++++++++++++++++++ 25 class CFunction 26 { 27 public: 28 void Init() 29 { 30 m_piArray = new int[10]; 31 } 32 33 public: 34 CFunction() : m_piArray(nullptr) {}; 35 ~CFunction() 36 { 37 if (nullptr != m_piArray) 38 { 39 delete[]m_piArray; 40 m_piArray = nullptr; 41 } 42 } 43 44 private: 45 int* m_piArray; 46 }; 47 48 //深拷贝================================================================ 49 class CDeepCopy 50 { 51 public: 52 CDeepCopy() 53 { 54 m_piArray = new int[10]; 55 } 56 57 CDeepCopy(const CDeepCopy& objDeepCopy) 58 { 59 m_piArray = new int[10]; 60 } 61 62 ~CDeepCopy() 63 { 64 if (nullptr != m_piArray) 65 { 66 delete[]m_piArray; 67 m_piArray = nullptr; 68 } 69 } 70 71 private: 72 int* m_piArray; 73 }; 74 75 76 int _tmain(int argc, _TCHAR* argv[]) 77 { 78 //使用默认的拷贝构造函数 79 CBase objBase; 80 //CBase objCopy(objBase);//编译器报错 81 82 //使用默认的拷贝构造函数 83 CFunction objFunction; 84 objFunction.Init(); 85 //CFunction objFncCopy(objFunction);//编译器报错 86 87 //深拷贝 88 CDeepCopy objDeepCopy; 89 CDeepCopy objDeepCopyCopy(objDeepCopy); 90 }
-
- 成员变量的初始化:构造函数内赋值与初始化参数列表的区别,这里就不过多介绍了,我们就当只有“初始化参数列表”这一个选项,一律使用它就好了,下面例子说明:
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 //初始化顺序:是按照成员变量的申明"至上而下"的赋值 7 //下面的初始化顺序:m_iData = 6; m_iMaxNum = 100; 8 CBase() : m_iMaxNum(100), m_iData(0) 9 { 10 11 } 12 13 ~CBase() 14 { 15 16 }; 17 18 private: 19 int m_iData; 20 const int m_iMaxNum;//可以对m_iMaxNum赋值 21 }; 22 23 int _tmain(int argc, _TCHAR* argv[]) 24 { 25 CBase objBase; 26 27 }
-
- explicit:用来禁止类的隐式转换,但只对一个参数(或者除了第一个参数外其余参数都有缺省值的多参构造函数)有用。就像成员变量的初始化一样,碰到只有一个参数的构造函数时,我们都加上explicit来限定,防止发生未知的隐式转换。
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 CBase() : m_iData(0){} 7 8 CBase(int iData) 9 { 10 m_iData = iData; 11 } 12 13 ~CBase() { }; 14 15 private: 16 int m_iData; 17 }; 18 19 //explicit============================ 20 class CTemp 21 { 22 public: 23 CTemp() : m_iData(0) 24 { 25 26 } 27 28 explicit CTemp(int iData) 29 { 30 31 m_iData = iData; 32 } 33 34 ~CTemp() {} 35 36 private: 37 int m_iData; 38 }; 39 40 int _tmain(int argc, _TCHAR* argv[]) 41 { 42 //常规赋值:将double隐式转换成int, 导致精度丢失 43 CBase objBase; 44 objBase = 3.4;//编译器会提示警告 45 46 //explicit:禁止隐式转换 47 CTemp objTemp; 48 //objTemp = 4.4; //编译器报错 49 }
-
- default:在构造和析构函数后面加上default, 编译器会为其自动生成默认的函数定义体,从而获得更高的代码执行效率,也可免除程序员手动定义该函数的工作量。
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 CBase() = default; 7 ~CBase() = default; 8 }; 9 10 int _tmain(int argc, _TCHAR* argv[]) 11 { 12 CBase objBase; 13 }
-
- delete:可以用来禁止某个函数被调用,常用来禁止编译器默认生成的拷贝构造函数和赋值函数,实际项目中像文件操作类,单例类的使用中很常见。
1 #include "stdafx.h" 2 3 4 class CSingle 5 { 6 public: 7 static CSingle& GetInstance() 8 { 9 static CSingle s_objSingle; 10 return s_objSingle; 11 } 12 13 void TestRun() 14 { 15 printf("The function is Running------------------------\n"); 16 } 17 public: 18 CSingle() = default; 19 CSingle(const CSingle&) = delete;//禁拷贝 20 CSingle& operator=(const CSingle&) = delete;//禁赋值 21 22 ~CSingle() = default; 23 }; 24 25 int _tmain(int argc, _TCHAR* argv[]) 26 { 27 CSingle::GetInstance().TestRun(); 28 29 //CSingle objCopy(CSingle::GetInstance());//使用拷贝构造函数,编译器报错 30 //CSingle objEqual = CSingle::GetInstance();//使用赋值构造函数,编译器报错 31 32 return 0; 33 }
(3)析构函数
类对象被销毁时会调用析构函数,用户可以在函数中销毁堆上申请的内存,等待事件, 等待线程退出等,而栈上的资源由编译器回收,下面展示等待线程退出的代码:
1 #include "stdafx.h" 2 #include <future> 3 4 class CBase 5 { 6 public: 7 void Init() 8 { 9 m_thread = std::async(std::launch::async, &CBase::DoThread, this); 10 } 11 12 void Uninit() 13 { 14 m_thread.wait(); 15 printf("wait thread exit\n"); 16 } 17 18 void DoThread() 19 { 20 std::this_thread::sleep_for(std::chrono::seconds(5)); 21 printf("thread exit\n"); 22 } 23 public: 24 CBase() 25 { 26 Init(); 27 } 28 29 ~CBase() 30 { 31 Uninit(); 32 } 33 34 private: 35 std::future<void> m_thread; 36 }; 37 38 int _tmain(int argc, _TCHAR* argv[]) 39 { 40 //在析构函数里等待线程退出 41 CBase objBase; 42 43 return 0; 44 }
(4)this指针
前面我们讲“类对象的创建和销毁”时提到过,成员函数在定义时,编译器会将非静态成员函数原型转换成带this指针形参的函数原型。这也就意味着我们在类作用域内可以访问类的任何非静态成员变量和函数,只是编译器帮我们隐藏了this指针的访问代码,下面我们将其展开,这样看就好理解它了, 大家可以关注下this指针的类型
(5)成员变量与成员函数
-
- 非静态成员变量和函数:创建类对象需要栈给成员变量申请存储空间,所以当成员变量为复杂大内存类型或类对象时,需要考虑是否定义为指针类型
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 //成员函数 7 void FnCall(int iData) 8 { 9 m_iData = iData; 10 } 11 12 public: 13 CBase() :m_iData(4) {} 14 ~CBase() {} 15 16 private: 17 //成员变量 18 int m_iData; 19 }; 20 21 int _tmain(int argc, _TCHAR* argv[]) 22 { 23 CBase objBase; 24 objBase.FnCall(10);//外部调用成员函数(只能调用 public成员函数) 25 26 return 0; 27 }
-
- const修饰非静态成员函数:函数内部不能再对非静态成员变量进行修改。想要防止成员变量被篡改,可以在函数后加const修饰,eg,一般在获取成员变量的函数中都会加const修饰。
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 void SetValue(int iData) 7 { 8 m_iData = iData; 9 } 10 11 int GetValue() const 12 { 13 //m_iData = 5; //编译器报错 14 return m_iData; 15 } 16 17 public: 18 CBase() :m_iData(0) {} 19 ~CBase() {} 20 21 private: 22 int m_iData; 23 }; 24 25 int _tmain(int argc, _TCHAR* argv[]) 26 { 27 CBase objBase; 28 objBase.SetValue(10); 29 printf("GetValue m_iData: %d\n", objBase.GetValue()); //10 30 31 return 0; 32 }
-
- 静态成员变量和函数(static):
1 #pragma once 2 class CBase 3 { 4 using CALLBACK = void(*)(void*); 5 6 public: 7 void SetValue(int iData); 8 int GetValue() const; 9 10 //成员函数作为回调 11 void FnCallBack(CALLBACK pfnCallback); 12 13 //成员函数作为回调必须为静态:如果不为静态,函数原型展开为void callback(void* pvArg, CBase* A) 14 //与申明的函数原型void(*)(void*)不匹配。 15 static void CallBack(void* pvArg); 16 17 //转换成非静态成员函数:这样就可以访问成员变量和函数 18 void DoCallBack(); 19 20 public: 21 CBase(); 22 ~CBase(); 23 24 private: 25 static int m_s_iData; 26 };
1 #include "stdafx.h" 2 #include "Base.h" 3 4 CBase::CBase() 5 { 6 } 7 8 9 CBase::~CBase() 10 { 11 } 12 13 //静态成员变量的初始化 14 int CBase::m_s_iData = 0; 15 void CBase::SetValue(int iData) 16 { 17 m_s_iData = iData; 18 } 19 20 int CBase::GetValue() const 21 { 22 return m_s_iData; 23 } 24 25 void CBase::FnCallBack(CALLBACK pfnCallback) 26 { 27 if (nullptr == pfnCallback) return; 28 29 pfnCallback((void*)this); 30 } 31 32 void CBase::CallBack(void* pvArg) 33 { 34 if (nullptr == pvArg) return; 35 36 CBase* pobjBase = static_cast<CBase*>(pvArg); 37 38 pobjBase->DoCallBack(); 39 } 40 41 void CBase::DoCallBack() 42 { 43 printf("CBase::m_s_iData: %d\n", m_s_iData); 44 }
1 #include "stdafx.h" 2 #include "Base.h" 3 4 5 int _tmain(int argc, _TCHAR* argv[]) 6 { 7 CBase objBase; 8 objBase.SetValue(10); 9 printf("GetValue m_iData: %d\n", objBase.GetValue()); //10 10 11 //静态成员函数作为回调 12 objBase.FnCallBack(CBase::CallBack); 13 14 return 0; 15 }
(6)类作用域::
-
- 快速地获取函数原型:用一个派生类的例子进行展示,不作说明,下文要讲到,也不建议设计代码时出现下面的代码
1 #include "stdafx.h" 2 3 class CBaseA 4 { 5 public: 6 void FnCall() 7 { 8 printf("CBaseA: FnCall\n"); 9 } 10 public: 11 CBaseA() = default; 12 virtual ~CBaseA() = default; 13 }; 14 15 class CBaseB 16 { 17 public: 18 void FnCall() 19 { 20 printf("CBaseB: FnCall\n"); 21 } 22 public: 23 CBaseB() = default; 24 virtual ~CBaseB() = default; 25 }; 26 27 //派生类:下一章将讲到========================================= 28 class CDerive : public CBaseA, public CBaseB 29 { 30 public: 31 void FnDerive() 32 { 33 CBaseA::FnCall(); 34 CBaseB::FnCall(); 35 } 36 public: 37 CDerive() = default; 38 virtual ~CDerive() = default; 39 }; 40 41 int _tmain(int argc, _TCHAR* argv[]) 42 { 43 CDerive objDerive; 44 objDerive.FnDerive(); 45 46 return 0; 47 }
-
- 应用场景:如果你了解MFC,会发现MFC封装了很多库,特别是对windows的很多API都进行封装。我们经常会在MFC项目中看见类似"::MessageBox(...)"这样的代码,这是调用windows库API,那我们就将其展开一下, 看看MFC对windows的MessageBox做了啥:
-
- 封装enum:有时候我们需定义一些常量,但其使用范围仅限在类作用域,我们可以考虑将常量封装在类内。即明确了使用范围,也缩减了类似"Common.h"的代码量。
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 typedef enum{EN_MIN = 10, EN_MAX = 100} EN_BASE_VALUE; 7 8 public: 9 void FnCall(EN_BASE_VALUE enBaseValue) 10 { 11 printf("CBase: enBaseValue: %d\n", enBaseValue); 12 } 13 14 public: 15 CBase() = default; 16 ~CBase() = default; 17 }; 18 19 int _tmain(int argc, _TCHAR* argv[]) 20 { 21 CBase objBase; 22 objBase.FnCall(CBase::EN_MIN); 23 24 return 0; 25 }
-
- 结合命名空间封装库:很多时候我在写完库代码,或比较简单的动态(静态)链接库后,考虑到库的函数可能与别的库或windows库的API冲突,一般我们会加上命名空间来标识我们的API。
1 #include "stdafx.h" 2 3 namespace UIBase 4 { 5 class CBase 6 { 7 public: 8 typedef enum{ EN_MIN = 10, EN_MAX = 100 } EN_BASE_VALUE; 9 10 public: 11 void FnCall(EN_BASE_VALUE enBaseValue) 12 { 13 printf("CBase: enBaseValue: %d\n", enBaseValue); 14 } 15 16 public: 17 CBase() = default; 18 ~CBase() = default; 19 }; 20 }; 21 22 //使用using namespace 解除作用域限定 23 //using namespace UIBase; //我们常用的using namespace std; 24 25 int _tmain(int argc, _TCHAR* argv[]) 26 { 27 //编译器报错 28 //CBase objBase; 29 //objBase.FnCall(CBase::EN_MIN); 30 31 UIBase::CBase objBase; 32 objBase.FnCall(UIBase::CBase::EN_MIN); 33 34 return 0; 35 }
(7)运算符重载
-
- 运算符重载:前面我们学习“函数模板的特化”时提到一个例子:通过特化的方式让两个结构体可以比较大小。而运算符重载的目的就是为了让我们的类具有“运算”功能,下面我们列出可以重载的运算符
+ | - | * | / | % | ^ |
& | | | ~= | ! | = | < |
> | += | -= | *= | /= | %= |
^= | &= | |= | << | >> | >>= |
<<= | == | != | <= | >= | && |
|| | ++ | -- | , | ->* | -> |
() | [] | new | delete | new[] | delete |
同时,运算符重载对参数个数是有限制的
运算符类型 | 参数限定 |
---|---|
单目运算符 | 如果实现为成员函数,则一般需要 0 个参数,如果实现为非成员函数,则一般需要 1 个参数 |
双目运算符 | 如果实现为成员函数,则一般需要 1 个参数,如果实现为非成员函数,则一般需要 2 个参数。 |
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 int GetValue() const 7 { 8 return m_iData; 9 } 10 11 public: 12 CBase() :m_iData(0){}; 13 explicit CBase(int iData) : m_iData(iData){}; 14 15 //注意:这里参数个数不能超过2 16 CBase operator +(CBase &objBase) 17 { 18 return CBase(m_iData + objBase.m_iData); 19 } 20 21 ~CBase() = default; 22 23 private: 24 int m_iData; 25 }; 26 27 int _tmain(int argc, _TCHAR* argv[]) 28 { 29 CBase objBase(4); 30 CBase objTemp(5); 31 CBase objOperator = objBase + objTemp; 32 printf("objOperator: m_iData : %d\n", objOperator.GetValue()); // 9 33 34 return 0; 35 }
-
- 仿函数:通过重载()运算符模拟函数形为的类,也就是说它不是函数, 因为它重载了()运算符,因此可以像调用函数一样对它进行调用。我们也叫它函数对象,在底层执行不同于函数指针的间接寻址,而是立即寻址的方式执行函数,速度更快。仿函数可以任意捕捉类内的成员变量和函数,且没有参数限制,在STL中被大量使用。eg.void sort(_RanIt _First, _RanIt _Last, _Pr _Pred)中“_Pr”就是一个仿函数
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 CBase() :m_iData(0){}; 7 explicit CBase(int iData) : m_iData(iData){}; 8 9 //重载(),无参数限制 10 int operator()(int iFirst, int iEnd) 11 { 12 return (iFirst + iEnd + m_iData); 13 } 14 15 ~CBase() = default; 16 17 private: 18 int m_iData; 19 }; 20 21 int _tmain(int argc, _TCHAR* argv[]) 22 { 23 CBase objBase(4); 24 25 printf("operator: %d\n", objBase(4, 2)); //10 26 27 return 0; 28 }
-
- lambda表达式:上面很多地方的代码都使用了lambda表达式,主要感觉是代码比较简洁优雅,不需要作一大堆的申明和定义函数。C++11之前,只有仿函数,然而它在泛型编程中使用中又比较广泛,于是C++11工作人员提出用lambda表达式来取代仿函数(这只是其中一个目的,eg,C++的lambda的编码方式更接近java的lambda),同时,lambda表达式可以在函数块内实现“闭包”,下面简单展示下,关于它的捕捉规则将放到后面详细进行说明。
1 #include "stdafx.h" 2 #include <array> 3 4 class CBase 5 { 6 public: 7 int GetValue() const 8 { 9 return m_iData; 10 } 11 12 public: 13 CBase() :m_iData(0){}; 14 explicit CBase(int iData) : m_iData(iData){}; 15 16 ~CBase() = default; 17 18 private: 19 int m_iData; 20 }; 21 22 int _tmain(int argc, _TCHAR* argv[]) 23 { 24 CBase objBase(2); 25 26 //函数块内进行封装,这只是展示它的实现,并不具备意义 27 auto atAlg = [&]() 28 { 29 return (objBase.GetValue() + 3 + 5); 30 }; 31 printf("atAlg: %d\n", atAlg()); 32 33 34 //lambda在容器中使用 35 std::array<int, 6> arArray = { 4, 6, 2, 8, 10, 7 }; 36 37 //将容器内数字进行排序,当然,我们这里可直接使用std::set,如果使用传统方式则需要自己编写for与swap函数 38 std::sort(arArray.begin(), arArray.end(), [](int iData, int iCompare) { return (iData > iCompare); }); 39 //打印一下 40 for (auto atData : arArray) 41 { 42 printf("arArray: %d\n", atData); 43 } 44 45 return 0; 46 }
(8)友元
详情请看友元函数和友元类,之所以不在这里展开,个人观点:类最大的特点之一就是它的封装性,而友元却破坏了它的封装性,将类数据变得不可靠,项目中经常碰到初学C++用不好它而造成维护成本的情况,不大爱用。
8. 类继承与多态
(1)类继承
-
- 类继承:前面我们提到函数时其特征之一是“代码复用”,而类继承可以看成是“一种更高级的代码复用”。类继承和类对象的访问限制一样分成"public,protected,private",但实际应用中基本上只用到"public",所以在这里就不总结其继承特性。还是结合代码了解下 什么是类继承
1 #include "stdafx.h" 2 #include <array> 3 4 class CBase 5 { 6 private: 7 int GetData() const 8 { 9 return m_iData; 10 } 11 12 public: 13 CBase() :m_iRow(1), m_iColumn(2), m_iData(3) 14 { 15 }; 16 ~CBase() = default; 17 18 public: 19 int m_iRow; 20 21 protected: 22 int m_iColumn; 23 24 private: 25 int m_iData; 26 }; 27 28 class CTemp : public CBase 29 { 30 public: 31 void FnCall() 32 { 33 //调用基本成员变量或函数 34 printf("CTemp: m_iRow: %d\n", m_iRow); 35 printf("CTemp: m_iColumn: %d\n", m_iColumn); 36 //printf("CTemp: m_iData: %d\n", m_iData);//编译器报错 37 //int iData = GetData();//编译器报错 38 39 //对比一下:CTemp成员函数内可以访问私有成员变量m_iTemp, 40 //却不能访问父类CBase的私有成员变量m_iData和私有成员函数GetData(); 为什么呢? 41 printf("CTemp: m_iTemp: %d\n", m_iTemp); 42 } 43 44 public: 45 CTemp() : m_iTemp(4){}; 46 ~CTemp(){}; 47 48 private: 49 int m_iTemp; 50 }; 51 52 int _tmain(int argc, _TCHAR* argv[]) 53 { 54 CTemp objTemp; 55 objTemp.FnCall(); 56 57 return 0; 58 }
-
- 类继承行为推导:为了方便理解,我们对派生的类进行“编译器行为推导”(非真正行为,只是用于理解的推导),如下图所示,当然编译器不仅仅做了这些,比如访问父类保护成员时,进行"继承标识的访问限制"转换等。当我们进行如下转换后,派生类继承了基类哪些资源,为什么派生类不能访问基类的私有成员变量和函数,我想这些问题就很容易解答,而不是去记规则。当然,这里肯定会有疑问,用“组合”就好了,干嘛要用继承,一切源于后面要说的“多态”。
-
- 构造与析构函数:还是结合上图进行说明,假设我们生成CTemp类对象objTemp,这时,栈上分配成员变量内存并存储,同时初始化参数列表(这就意味着要构造CBase对象objBase,初始化CBase的参数列表,并调用CBase的构造函数,生成对象objBase),当CTemp参数列表初始化完成后,调用CTemp构造函数,生成CTemp对象objTemp。而当我们销毁CTemp对象时,会调用CTemp析构函数(用户回收堆上资源),当析构调用结束,栈回收资源,这时,CBase对象将被销毁,从而调用CBase析构函数。我们理解了派生类的创建和销毁过程后,就不用再去记“派生与父类构造,析构的调用顺序了”。
- 类型转换:
- 在我们设计代码时,特别是使用"工厂模式"时,通常我们将基类指针作为形参,在函数体内new具体的派生类。这里就有一个问题:基类指针为什么能new派生类?这是因为我们的编译器默认了一种自动类型转换方式(向上类型转换:派生类指针或引用转换成基类表示),那编译器为什么不弄个向下类型转换呢?我们还是结合例子来进行说明这种转换
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 void FnBase() 7 { 8 printf("FnBase============\n"); 9 } 10 11 public: 12 CBase() = default; 13 ~CBase() = default; 14 }; 15 16 //派生================================================= 17 class CDerive : public CBase 18 { 19 public: 20 void FnDerive() 21 { 22 m_iData = 10; 23 printf("FnDerive++++++++++++\n"); 24 } 25 26 public: 27 CDerive() : m_iData(0){}; 28 ~CDerive() = default; 29 30 private: 31 int m_iData; 32 }; 33 34 int _tmain(int argc, _TCHAR* argv[]) 35 { 36 CBase objBase; 37 CDerive objDerive; 38 39 //向上转换:派生类指针向父类指针转换,编译器允许 40 CBase* pobjBase = &objDerive; 41 pobjBase->FnBase(); 42 43 //向下转换:父类指针向派生类指针转换,编译器不允许,需要手动干预,存在较大风险 44 //CDerive* pobjDerive = &objBase; 编译器报错 45 CDerive* pobjDerive = (CDerive*)&objBase;//这里不报错 46 //pobjDerive->FnDerive(); 执行时内存报错 47 48 return 0; 49 }
1 #include "stdafx.h" 2 #include <vector> 3 4 //这里就不重写构造与析构函数了,使用系统默认生成的,不然代码太多了 5 //定义接口 6 class IProduct 7 { 8 public: 9 virtual void FnCall() = 0; 10 }; 11 12 //具体的产品接口 13 class CProductA : public IProduct 14 { 15 public: 16 virtual void FnCall() override 17 { 18 printf("CProductA: FnCall\n"); 19 } 20 }; 21 22 class CProductB : public IProduct 23 { 24 public: 25 virtual void FnCall() override 26 { 27 printf("CProductB: FnCall\n"); 28 } 29 }; 30 31 class CProductC : public IProduct 32 { 33 virtual void FnCall() override 34 { 35 printf("CPoductC: FnCall\n"); 36 } 37 }; 38 39 //工厂类 40 class CFactory 41 { 42 public: 43 44 using VECPRODUCT = std::vector<IProduct*>; 45 46 static CFactory& GetInstance() 47 { 48 static CFactory s_objFactory; 49 return s_objFactory; 50 } 51 52 VECPRODUCT GetProductVec() const 53 { 54 return m_vecProduct; 55 } 56 57 private: 58 void RegisterProduct(IProduct* pobjProduct) 59 { 60 if (nullptr == pobjProduct) return; 61 62 auto atIter = std::find(m_vecProduct.begin(), m_vecProduct.end(), pobjProduct); 63 if (m_vecProduct.end() == atIter) 64 { 65 m_vecProduct.push_back(pobjProduct); 66 } 67 } 68 69 void UnRegisterProduct() 70 { 71 for (auto atIter : m_vecProduct) 72 { 73 if (nullptr != atIter) 74 { 75 delete atIter; 76 atIter = nullptr; 77 } 78 } 79 m_vecProduct.erase(m_vecProduct.begin(), m_vecProduct.end()); 80 } 81 82 public: 83 CFactory() 84 { 85 RegisterProduct(new CProductA); 86 RegisterProduct(new CProductB); 87 RegisterProduct(new CProductC); 88 } 89 CFactory(const CFactory& objFactory) = delete; //禁止拷贝 90 CFactory& operator = (const CFactory& objFactory) = delete; //禁止赋值 91 ~CFactory() 92 { 93 UnRegisterProduct(); 94 } 95 96 private: 97 VECPRODUCT m_vecProduct; 98 }; 99 100 101 int _tmain(int argc, _TCHAR* argv[]) 102 { 103 //获取具体工厂执行函数 104 auto atVecProduct = CFactory::GetInstance().GetProductVec(); 105 for (auto atProduct : atVecProduct) 106 { 107 atProduct->FnCall(); 108 } 109 110 return 0; 111 }
3. 使用“向上类型转换“也可能引发一些问题,比如下面的例子就会造成内存泄漏,这是个比较经典的问题:为什么要在析构函数前面加virtual?这里又弄出了一个概念“静态绑定与动态绑定”,下文说明
1 #include "stdafx.h" 2 #include <crtdbg.h> 3 #include <typeinfo> 4 5 class CBase 6 { 7 public: 8 CBase() 9 { 10 m_piBase = new int[10]; 11 } 12 13 ~CBase() 14 { 15 if (nullptr != m_piBase) 16 { 17 delete []m_piBase; 18 m_piBase = nullptr; 19 } 20 } 21 22 private: 23 int* m_piBase; 24 }; 25 26 class CDerive : public CBase 27 { 28 public: 29 CDerive() 30 { 31 m_piDerive = new int[10]; 32 } 33 34 ~CDerive() 35 { 36 if (nullptr != m_piDerive) 37 { 38 delete[]m_piDerive; 39 m_piDerive = nullptr; 40 } 41 } 42 43 private: 44 int* m_piDerive; 45 }; 46 47 int _tmain(int argc, _TCHAR* argv[]) 48 { 49 _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 50 51 CBase* pobjBase = new CDerive; 52 53 printf("pobjBase type: %s\n", typeid(*pobjBase).name());//class CBase 54 55 delete pobjBase; 56 pobjBase = nullptr; 57 58 //调试出现内存泄漏 59 return 0; 60 }
-
- 静态绑定: 我们先结合“3.”的例子说明静态编译,所谓的静态编译就是在编译时期就确定了指针类型,我们知道编译器具有自动“向上类型转换”的特性,所以CBase* pobjBase = new CDerive成立,但同时编译器不支持自动“向下类型转换”的特性,这就意味在编译时期,pobjBase所指向的类型确实为CBase,delete pobjBase也就只调用CBase的析构函数,eg.我们在CDerive里写一个新的函数,pobjBase寻址不到。
- 动态绑定:我们结合下文的例子:动态绑定只有在“多态”特性下才执行,区别于静态绑定,在运行时期才确定指针或引用类型(像MFC的六大特性之一:RTTI就是讲的这个)。我们先简单说明下这个例子,具体的在后文进行展开,在定义CBase时会生成一个虚函数表用来存放虚函数地址,而在定义CDerive时也会生成一个虚函数表,在编译时期确定要调用哪个虚函数(同一个函数两个地址),当CBase* pobjBase = new CDerive时,首先生成一个虚表指针,再在CDrive虚函数表里会存放CDrive析构函数的入口地址,delete pobjBase时,虚表指针会从虚函数表里查找是否有CDerive的析构函数,如果有,则调用的是CDerive的析构函数,从而确定pobjBase所指向的类型为CDerive。下文我们就准备讲虚函数表和虚表指针
1 #include "stdafx.h" 2 #include <crtdbg.h> 3 #include <typeinfo> 4 5 class CBase 6 { 7 public: 8 CBase() 9 { 10 m_piBase = new int[10]; 11 } 12 13 virtual ~CBase() 14 { 15 if (nullptr != m_piBase) 16 { 17 delete []m_piBase; 18 m_piBase = nullptr; 19 } 20 } 21 22 private: 23 int* m_piBase; 24 }; 25 26 class CDerive : public CBase 27 { 28 public: 29 CDerive() 30 { 31 m_piDerive = new int[10]; 32 } 33 34 virtual ~CDerive() 35 { 36 if (nullptr != m_piDerive) 37 { 38 delete[]m_piDerive; 39 m_piDerive = nullptr; 40 } 41 } 42 43 private: 44 int* m_piDerive; 45 }; 46 47 int _tmain(int argc, _TCHAR* argv[]) 48 { 49 _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 50 51 CBase* pobjBase = new CDerive; 52 53 printf("pobjBase type: %s\n", typeid(*pobjBase).name());//class CDerive 54 55 delete pobjBase; 56 pobjBase = nullptr; 57 58 //没有内存泄漏 59 return 0; 60 }
(2)多态
前面讲类继承时,尽量没有使用“virtual”标识符是为将“类继承”的概念讲清楚,然后再来讲讲类继承中的“多态”特性。C++有个“函数重载”特性,主要重载的是函数形参,通过”调用不同的函数原型,来进入不同的函数体执行“。而我们要讲的类继承的“多态”,则是”使用同一个函数原型,执行不同的函数体“,这就有点意思了,下面我展开讲讲它是怎么做到的。
-
- 虚函数:简单来说,就是带有“virtual”标识符的类成员函数。还是用代码展示下虚函数
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 virtual void FnCall() 7 { 8 printf("CBase: FnCall\n"); 9 } 10 11 public: 12 CBase() = default; 13 virtual ~CBase() = default; 14 }; 15 16 class CDerive : public CBase 17 { 18 public: 19 //override:由C++11提出,用来检测虚函数原型书写语法 20 virtual void FnCall() override 21 { 22 printf("CDerive: FnCall\n"); 23 } 24 25 public: 26 CDerive() = default; 27 virtual ~CDerive() = default; 28 }; 29 30 int _tmain(int argc, _TCHAR* argv[]) 31 { 32 CDerive* pobjDerive = new CDerive; 33 pobjDerive->FnCall(); 34 35 //向上类型转换:可直接赋值,编译器隐式转换 向下类型转换:有virtual,使用dynamic_cast 36 CBase* pobjBase = pobjDerive; 37 pobjBase->FnCall();//根据RTTI,虚表指针指向的是CDerive:FnCall 38 39 pobjBase->CBase::FnCall();//可以指定作用域跳转虚表指针 40 41 delete pobjDerive; 42 pobjDerive = nullptr; 43 44 return 0; 45 }
-
- 虚函数表和虚表指针:这里我们需要对虚函数进行剖析,就以CBase为例,在定义 CBase时,我们继续推导下“编译器行为”(非真正行为),如图所示!意思是每个带有虚函数的类在定义时都会生成一张虚函数表__vfptr用来存放虚函数“FnTest, FnCall, ~CBase”的入口地址,同时编译器还会定义一个虚表指针pvtr用来指向函数表__vfptr,在类对象调用虚函数时,是通过虚表指针来获取虚函数的入口地址。
-
- 虚函数调用:上文我们讲到每个带有虚函数的类都会生成一张虚函数表__vfptr和一个虚表指针pvtr,那么,当存在类继承行为时,虚函数表和虚表指针又存在哪些行为,如下图所示!从CDerive的虚函数表中我们可以看出:FnTest没有被重写,CBase的FnTest地址与CDerive的FnTest地址相同,说明如果基类的虚函数没有被派生类重写,那么派生类将沿用基类的虚函数地址。如果重写,那就各用各的地址,从FnCall地址可以看出,我们再结合代码看看虚函数的调用。
1 #include "stdafx.h" 2 3 class CBase 4 { 5 public: 6 virtual void FnTest() 7 { 8 printf("CBase: test\n"); 9 } 10 11 virtual void FnCall() 12 { 13 printf("CBase: FnCall\n"); 14 } 15 16 public: 17 CBase() = default; 18 virtual ~CBase() = default; 19 }; 20 21 class CDerive : public CBase 22 { 23 public: 24 25 virtual void FnCall() override 26 { 27 printf("CDerive: FnCall\n"); 28 } 29 30 public: 31 CDerive() = default; 32 virtual ~CDerive() = default; 33 }; 34 35 int _tmain(int argc, _TCHAR* argv[]) 36 { 37 CBase objBase; 38 CDerive objDerive; 39 objDerive.FnCall();//CDerive: FnCall 40 objDerive.FnTest();//CBase: test 41 42 return 0; 43 }
-
- 纯虚函数与抽象类:所谓的纯虚函数,就是在虚函数后面加上“=0”即可,而抽象类就是带有纯虚函数的类,并且抽象类不能拥有实例对象。那么它的意义是什么,实现“接口与实现分离"。我们还是结合例子进行说明,eg,当上位机与下位机通信,需要同时用到usb,lan,uart和spi,而上位机只关心驱动接口的”开,关,读,写“时,我们要封装驱动接口,只需要开放上位机想要的几个API,其它细节封装到具体的驱动接口中即可,我们还是使用代码展示。
1 #include "stdafx.h" 2 #include <vector> 3 #include <future> 4 5 //抽象类:提供高度统一的接口 6 class IDriver 7 { 8 public: 9 virtual bool Open(const char* pcName) = 0; 10 virtual int Read(char acBuffer[], int iSizeBuffer) = 0; 11 virtual int write(const char* pcData, int iDataSize) = 0; 12 virtual bool Close() = 0; 13 14 public: 15 IDriver() = default; 16 virtual ~IDriver() = default; 17 }; 18 19 //具体驱动实现类======================================================= 20 class CUsb : public IDriver 21 { 22 public: 23 virtual bool Open(const char* pcName) override { return true; } 24 virtual int Read(char acBuffer[], int iSizeBuffer) override { return 1; } 25 virtual int write(const char* pcData, int iDataSize) override { return 1; } 26 virtual bool Close() override { return true; } 27 //private: 28 //在这里封装USB的参数配置... 29 public: 30 CUsb() = default; 31 virtual ~CUsb() = default; 32 }; 33 34 class CUart : public IDriver 35 { 36 public: 37 virtual bool Open(const char* pcName) override { return true; } 38 virtual int Read(char acBuffer[], int iSizeBuffer) override { return 1; } 39 virtual int write(const char* pcData, int iDataSize) override { return 1; } 40 virtual bool Close() override { return true; } 41 42 public: 43 CUart() = default; 44 virtual ~CUart() = default; 45 }; 46 47 class CLan : public IDriver 48 { 49 public: 50 virtual bool Open(const char* pcName) override { return true; } 51 virtual int Read(char acBuffer[], int iSizeBuffer) override { return 1; } 52 virtual int write(const char* pcData, int iDataSize) override { return 1; } 53 virtual bool Close() override { return true; } 54 55 public: 56 CLan() = default; 57 virtual ~CLan() = default; 58 }; 59 60 class CSpi : public IDriver 61 { 62 public: 63 virtual bool Open(const char* pcName) override { return true; } 64 virtual int Read(char acBuffer[], int iSizeBuffer) override { return 1; } 65 virtual int write(const char* pcData, int iDataSize) override { return 1; } 66 virtual bool Close() override { return true; } 67 68 public: 69 CSpi() = default; 70 virtual ~CSpi() = default; 71 }; 72 73 //继承搞个工厂管理类用来温习下前面的内容 74 class CFactory 75 { 76 public: 77 using VECDRIVER = std::vector<IDriver*>; 78 79 static CFactory& GetInstance() 80 { 81 static CFactory s_objFactory; 82 return s_objFactory; 83 } 84 85 VECDRIVER GetVecDriver() const 86 { 87 return m_vecDriver; 88 } 89 90 private: 91 void RegisterDriver(IDriver* pobjDriver) 92 { 93 auto atDriver = std::find(m_vecDriver.begin(), m_vecDriver.end(), pobjDriver); 94 if (m_vecDriver.end() == atDriver) 95 { 96 m_vecDriver.push_back(pobjDriver); 97 } 98 } 99 void UnRegisterDriver() 100 { 101 for (auto atDriver : m_vecDriver) 102 { 103 delete atDriver; 104 atDriver = nullptr; 105 } 106 m_vecDriver.erase(m_vecDriver.begin(), m_vecDriver.end()); 107 } 108 109 public: 110 CFactory() 111 { 112 RegisterDriver(new CUsb); 113 RegisterDriver(new CUart); 114 RegisterDriver(new CLan); 115 RegisterDriver(new CSpi); 116 } 117 CFactory(const CFactory& objFactory) = delete; 118 CFactory operator=(const CFactory& objFactory) = delete; 119 ~CFactory() 120 { 121 UnRegisterDriver(); 122 } 123 private: 124 VECDRIVER m_vecDriver; 125 }; 126 127 int _tmain(int argc, _TCHAR* argv[]) 128 { 129 //上位机需要同时调用这几个接口来实现逻辑 130 typedef enum{EN_BUFF_SIZE = 1024}EN_INFO; 131 132 bool bExitThread = false; 133 CFactory::VECDRIVER vecDriver = CFactory::GetInstance().GetVecDriver(); 134 135 //打开所有驱动 136 for (auto atDriver : vecDriver) 137 { 138 if (!atDriver->Open("")){/*直接弹窗报错*/} 139 } 140 141 //创建一个线程来读取驱动信息 142 auto atThread = std::async(std::launch::async, [&]() 143 { 144 char acBuffer[EN_BUFF_SIZE] = { 0 }; 145 146 while (1) 147 { 148 for (auto atDirver : vecDriver) 149 { 150 int iReadSize = atDirver->Read(acBuffer, EN_BUFF_SIZE); 151 if (0 < iReadSize) 152 { 153 //doing something... 154 //给下位机回消息 155 int iWriteSize = atDirver->write("i recv info", sizeof("i recv info")); 156 //if (iWriteSize != sizeof("i recv info")) //写入失败 157 } 158 } 159 160 if (bExitThread) break; 161 162 std::this_thread::sleep_for(std::chrono::milliseconds(1));// 163 } 164 }); 165 166 //模拟下等待线程退出 167 std::this_thread::sleep_for(std::chrono::seconds(5)); 168 bExitThread = true; 169 170 atThread.wait();//阻塞等待 171 printf("wait thread exit\n"); 172 173 //关闭驱动 174 for (auto atDriver : vecDriver) 175 { 176 atDriver->Close(); 177 } 178 179 return 0; 180 }
9. 类模板
我们在讲”函数“这一章时,研究过”函数模板“这个知识,那么,我们就再看类模板就可以简单了,其实类模板我们可以理解成:把我们需要使用的函数模板进行包装,然后以类对象的形式进行调用。
-
- 类模板原型:类模板与函数模板一样,类模板在创建对象时,栈根据模板类型申请对应的成员变量的存储空间,而成员函数在类对象调用时,才生成函数原型,并将对应的函数体拷贝到”代码区“去执行。下面我们通过例子来简单展示下,同时,我们也把之前学的函数模板在普通类中的使用例子也展示下,对比下,可以考虑在设计代码时,该用哪种方式进行套用。
1 #include "stdafx.h" 2 #include <iostream> 3 4 template<typename T, typename U> 5 class CAlg 6 { 7 public: 8 template<typename T> 9 T Plus(T t, T u) 10 { 11 return (t + u); 12 } 13 14 template<typename T> 15 T Sub(T t, T u) 16 { 17 return (t - u); 18 } 19 20 template<typename T> 21 T Ride(T t, T u) 22 { 23 return (t * u) 24 } 25 26 template<typename T> 27 T Div(T t, T u) 28 { 29 return (t / u); 30 } 31 32 //不同类型的数据相加:本意是想T,U按照编译器规则自动进行转换 33 //template<typename T, typename U> 34 //auto Plus(T t, U u) //编译器报错 35 //{ 36 // return (t + u); 37 //} 38 39 template<typename T, typename U> 40 auto Plus(T t, U u)->decltype(t + u) //类型擦除 41 { 42 return (t + u); 43 } 44 public: 45 void FnCall() 46 { 47 printf("CBase: FnCall\n"); 48 } 49 50 public: 51 CAlg() = default; 52 ~CAlg() = default; 53 }; 54 55 //普通类的函数模板==================================================== 56 class CBase 57 { 58 public: 59 template<typename T, typename U> 60 auto Plus(T t, U u)->decltype(t + u) 61 { 62 return (t + u); 63 } 64 65 public: 66 CBase() = default; 67 ~CBase() = default; 68 }; 69 70 int _tmain(int argc, _TCHAR* argv[]) 71 { 72 //创建类模板对象:这里必须要确定其类型 73 CAlg<int, double> objAlg; 74 75 //普通函数的调用 76 objAlg.FnCall(); 77 78 //同类型相加 79 auto atPlus = objAlg.Plus(2, 4);// 6 80 std::cout << atPlus << std::endl; 81 82 //不同类型相加 83 auto atPlusD = objAlg.Plus(2.4, 2); //4.4 84 std::cout << atPlusD << std::endl; 85 86 //普通类的函数模板============================================== 87 CBase objBase; 88 std::cout << objBase.Plus(2, 4) << std::endl; // 6 89 std::cout << objBase.Plus(3, 4.5) << std::endl;// 7.5 90 std::cout << objBase.Plus(4.5, 3) << std::endl;// 7.5 91 92 return 0; 93 }
-
- 类模板实例化:与函数模板无异,也分“隐式实例化”和“显示实例化”,显示实例化让类模板具备普通类特性,但调用时需要拷贝函数体到“代码区”,见代码实例。
1 #include "stdafx.h" 2 #include <iostream> 3 4 template<typename T> 5 class CBase 6 { 7 public: 8 void FnCall(T t) 9 { 10 std::cout << t << std::endl; 11 } 12 13 public: 14 CBase() = default; 15 explicit CBase(T t) {}; 16 ~CBase() = default; 17 }; 18 19 //显示实例化 20 template class CBase<double>; 21 22 int _tmain(int argc, _TCHAR* argv[]) 23 { 24 CBase<int> objBase; //隐式实例化 25 objBase.FnCall(4); 26 27 //显示调用 28 CBase<double> objBaseD; 29 objBaseD.FnCall(4.4);//区别于隐式实例化,FnCall已有函数原型,不需要推导生成 30 31 return 0; 32 }
-
- 类模板特化(具体化):之所以要进行“特化”,模板不能满足所有类型的执行逻辑,需要对特别的类型进行单独处理。特化一般分为”全特化“和”偏特化”,我们知道函数模板是不支持”偏特化“的,但类模板支持“偏特化”,这也就意味着类成员函数也能实现偏特化。
1 #include "stdafx.h" 2 #include <iostream> 3 #include <typeinfo> 4 5 template<typename T> 6 class CBase 7 { 8 public: 9 void FnCall(T t) 10 { 11 std::cout << t << std::endl; 12 } 13 14 public: 15 CBase() = default; 16 ~CBase() = default; 17 }; 18 19 //整个类的全特化 20 template<> 21 class CBase<int> 22 { 23 public: 24 void FnCall(int iData)//函数体重写代码逻辑 25 { 26 std::cout << typeid(iData).name() << std::endl; 27 } 28 29 public: 30 CBase() = default; 31 ~CBase() = default; 32 }; 33 34 //对单个成员函数进行全特化 35 template<> 36 void CBase<char>::FnCall(char cData) 37 { 38 std::cout << typeid(cData).name() << std::endl; 39 } 40 41 int _tmain(int argc, _TCHAR* argv[]) 42 { 43 CBase<double> objBase; 44 objBase.FnCall(4.4); 45 46 //整个类的全特化调用 47 CBase<int> objBaseI; 48 objBaseI.FnCall(4); 49 50 //单个成员函数的全特化 51 CBase<char> objBaseC; 52 objBaseC.FnCall(20); 53 54 return 0; 55 }
1 #include "stdafx.h" 2 #include <iostream> 3 #include <typeinfo> 4 #include <vector> 5 6 //部分参数偏特化 7 template<typename T, int iNum> 8 class CBase 9 { 10 public: 11 void FnCall() 12 { 13 for each(auto atData in m_aArray) 14 { 15 std::cout << atData << std::endl; 16 } 17 } 18 19 //成员函数的偏特化 20 bool FnTest(T t, char cNum) 21 { 22 return (typeid(t) == typeid(cNum)); 23 } 24 25 private: 26 void Init() 27 { 28 for (int iCount = 0; iCount < iNum; iCount++) 29 { 30 m_aArray[iCount] = iCount; 31 } 32 } 33 34 public: 35 CBase() { Init(); }; 36 ~CBase() = default; 37 38 private: 39 T m_aArray[iNum]; 40 }; 41 42 //===================================================== 43 //模板参数特化为另一个模板类 44 template<typename T, typename U> 45 class CMyBase 46 { 47 public: 48 void FnCall(std::vector<T> && t, std::vector<U> && u) 49 { 50 for (auto atData : t) 51 { 52 std::cout << atData << std::endl; 53 } 54 55 for (auto atVec : u) 56 { 57 std::cout << atVec << std::endl; 58 } 59 } 60 61 public: 62 CMyBase() = default; 63 ~CMyBase() = default; 64 }; 65 66 int _tmain(int argc, _TCHAR* argv[]) 67 { 68 //偏特化出一个数组 69 CBase<int, 10> objBase; 70 objBase.FnCall(); 71 bool bRet = objBase.FnTest(4, 10); //false 72 73 // 74 std::vector<double> vecChar = { 1.1, 3.1, 5.1, 7.1, 9.1 }; 75 std::vector<int> vecInt = { 2, 4, 6, 8, 10 }; 76 77 CMyBase<double, int> objMyBase; 78 objMyBase.FnCall(std::move(vecChar), std::move(vecInt));//传入右值,不生成副本 79 80 return 0; 81 }
1 #include "stdafx.h" 2 #include <array> 3 4 //主模板 5 template<typename T, typename U> 6 class CBase 7 { 8 public: 9 bool FnCall(T t, U u) 10 { 11 return (t > u); 12 } 13 14 public: 15 CBase() = default; 16 ~CBase() = default; 17 }; 18 19 //=================================================== 20 //偏特化为指针, 21 //偏特化类时, 不能没有主模板, 只有偏特化 22 template<typename T, typename U> 23 class CBase<T*, U*> 24 { 25 public: 26 bool FnCall(T* t, U* u) 27 { 28 return (*t > *u); 29 } 30 31 public: 32 CBase() = default; 33 ~CBase() = default; 34 }; 35 36 //===================================================== 37 //偏特化为引用 38 template<typename T, typename U> 39 class CBase<T&, U&> 40 { 41 public: 42 bool FnCall(T& t, U& u) 43 { 44 return (t > u); 45 } 46 47 public: 48 CBase() = default; 49 ~CBase() = default; 50 }; 51 52 //===================================================== 53 //偏特化为右值引用 54 template<typename T, typename U> 55 class CBase<T&&, U&&> 56 { 57 public: 58 bool FnCall(T && t, U && u) 59 { 60 return (t > u); 61 } 62 63 public: 64 CBase() = default; 65 ~CBase() = default; 66 }; 67 68 69 int _tmain(int argc, _TCHAR* argv[]) 70 { 71 int iSrc = 4; 72 int iDst = 5; 73 74 //主模板 75 CBase<int, int> objBase; 76 objBase.FnCall(iSrc, iDst);//false 77 78 //偏特化为指针 79 CBase<int*, int*> objPoint; 80 objPoint.FnCall(&iSrc, &iDst);//false 81 82 //偏特化为引用 83 CBase<int&, int&> objQuote; 84 objQuote.FnCall(iSrc, iDst);//false 85 86 //偏特化为右值引用 87 CBase<int&&, int&&> objQuoteRight; 88 objQuoteRight.FnCall(4, 5);//false 89 objQuoteRight.FnCall(std::move(iSrc), std::move(iDst));//false 右值 90 objQuoteRight.FnCall(std::forward<int>(iSrc), std::forward<int>(iDst));//false 左值 91 92 return 0; 93 }
-
- 类模板继承:一般分为两种情况 :1. 类模板派生为普通类时,必须实例化基类;2. 类模板派生为类模板时,不需要实例化基类。我们先复习下派生类的内容,再结合编译器行为分析。
-
- 类模板的编译:我前面的演示代码写的很不规范,理论上我们要写一个类都是用两个文件".h"和".cpp",其中".h"文件写类的成员函数申明和成员变量,".cpp"写类的成员函数定义。然后,我们会发现这样写模板类时编译器会报错???原因是编译器在进行分离编译时,找不到函数原型而报错,下面我们再具体展开下:
- 我们知道"编译"是将工程中所有的源文件(.cpp)分开单独进行编译,最后再"链接"成可执行文件,这也就意味着".h"文件不参与编译。但我们一般会用".cpp"文件包含".h"文件,在"预处理"阶段进行展开,也就是 将".h"文件的内容插到".cpp"里,从而理论上实现".h"文件内容的”编译“;
- 然后我们再看看".cpp"编译时做了些啥?编译器在编译成员函数定义时,首先从展开的".h"文件的内容里找到函数原型,然后分析函数定义里的语法,词法,如果没有问题再将函数定义拷贝到”代码区“。这也就意味着展开的的".h"文件的内容只是用来查找成员函数原型和成员变量类型,并不参与实际的编译工作,也就是说".h"文件里的函数定义是不参与编译的。(eg.你可以在.h文件里定义一个函数,然后写入inta2 i;如果不调用这个函数,编译器不会报错);
- 额?那么我们就可以把模板类的成员函数的定义写到".h"文件里,这样编译器就不会报错了,而且我们知道模板类生成对象时才生成具体的成员函数原型和成员变量类型,这样就完全没有问题了。因而C++工作人员为了单独处理类模板的这个特点(类似类模板的全特化),新加入了".hpp"文件类型,表示将类定义与类成员函数定义放在一个文件里。普通类可不建议你这么用哦,不然后没有”语法检测“的福利了。
1 #ifndef _OBSERVER_H__ 2 #define _OBSERVER_H__ 3 4 #include "stdafx.h" 5 #include <vector> 6 7 template<typename ReturnT, typename ParamT> 8 class IReceiverImplBase; 9 10 //观察者接口 11 template<typename ReturnT, typename ParamT> 12 class IObserverImplBase 13 { 14 public: 15 virtual void AddReceiver(IReceiverImplBase<ReturnT, ParamT>* pobjReceiver) = 0; 16 virtual void RemoveReceiver(IReceiverImplBase<ReturnT, ParamT>* pobjReceiver) = 0; 17 virtual ReturnT Broadcast(ParamT param) = 0; 18 virtual ReturnT Notify(ParamT param) = 0; 19 20 public: 21 IObserverImplBase() = default; 22 virtual ~IObserverImplBase() = default; 23 }; 24 25 //观察者接口实现 26 template<typename ReturnT, typename ParamT> 27 class CObserverImpl : public IObserverImplBase<ReturnT, ParamT> 28 { 29 public: 30 using ReceiverVector = std::vector<IReceiverImplBase<ReturnT, ParamT>*>; 31 32 virtual void AddReceiver(IReceiverImplBase<ReturnT, ParamT>* pobjReceiver) override 33 { 34 if (nullptr == pobjReceiver) return; 35 36 m_vecReceiver.push_back(pobjReceiver); 37 pobjReceiver->AddObserver(this); 38 } 39 40 virtual void RemoveReceiver(IReceiverImplBase<ReturnT, ParamT>* pobjReceiver) override 41 { 42 if (nullptr == pobjReceiver) return; 43 44 auto atReceiver = std::find(m_vecReceiver.begin(), m_vecReceiver.end(), pobjReceiver); 45 if (m_vecReceiver.end() != atReceiver) 46 { 47 m_vecReceiver.erase(atReceiver); 48 } 49 } 50 51 virtual ReturnT Broadcast(ParamT param) override 52 { 53 for (auto atReceiver : m_vecReceiver) 54 { 55 atReceiver->Receive(param); 56 } 57 58 return ReturnT(); 59 } 60 61 virtual ReturnT Notify(ParamT param) override 62 { 63 for (auto atReceiver : m_vecReceiver) 64 { 65 atReceiver->Respond(param, this); 66 } 67 68 return ReturnT(); 69 } 70 71 public: 72 CObserverImpl() = default; 73 virtual ~CObserverImpl() = default; 74 75 protected: 76 ReceiverVector m_vecReceiver; 77 }; 78 79 //============================================================================== 80 //接收者接口 81 template<typename ReturnT, typename ParamT> 82 class IReceiverImplBase 83 { 84 public: 85 virtual void AddObserver(IObserverImplBase<ReturnT, ParamT>* pobjObserver) = 0; 86 virtual void RemoveObserver() = 0; 87 virtual ReturnT Receive(ParamT param) = 0; 88 virtual ReturnT Respond(ParamT param, IObserverImplBase<ReturnT, ParamT>* pobjObserver) = 0; 89 90 public: 91 IReceiverImplBase() = default; 92 virtual ~IReceiverImplBase() = default; 93 }; 94 95 //接收者接口实现 96 template<typename ReturnT, typename ParamT> 97 class CReceiverImpl : public IReceiverImplBase<ReturnT, ParamT> 98 { 99 public: 100 using ObserverVector = std::vector<IObserverImplBase<ReturnT, ParamT>*>; 101 102 virtual void AddObserver(IObserverImplBase<ReturnT, ParamT>* pobjObserver) override 103 { 104 if (nullptr == pobjObserver) return; 105 106 m_vecObserver.push_back(pobjObserver); 107 } 108 109 virtual void RemoveObserver() override 110 { 111 for (auto atObserver : m_vecObserver) 112 { 113 atObserver->RemoveReceiver(this); 114 } 115 } 116 117 virtual ReturnT Receive(ParamT param) override 118 { 119 return ReturnT(); 120 } 121 122 virtual ReturnT Respond(ParamT param, IObserverImplBase<ReturnT, ParamT>* pobjObserver) override 123 { 124 return ReturnT(); 125 } 126 127 public: 128 CReceiverImpl() = default; 129 virtual ~CReceiverImpl() = default; 130 131 protected: 132 ObserverVector m_vecObserver; 133 }; 134 135 #endif /*_OBSERVER_H__*/
1 #include "stdafx.h" 2 #include "observer.hpp" 3 #include <string> 4 #include <memory> 5 6 //调用实例:只是简单套用框架使用,并非像"菜单"按钮实现那么复杂 7 typedef struct 8 { 9 int bkColor; 10 int bkImage; 11 std::string strCtrlName; 12 }ST_BKINFO, *PST_BKINFO; 13 14 //观察者实例 15 class CObserverManager : public CObserverImpl<bool, PST_BKINFO> 16 { 17 public: 18 static CObserverManager& GetInstance() 19 { 20 static CObserverManager s_objObserverManager; 21 return s_objObserverManager; 22 } 23 24 void GetReceiverInfo(PST_BKINFO pstBkInfo) 25 { 26 if (nullptr == pstBkInfo) return; 27 28 printf("control name:%s bkcolor:%d bkimage: %d\n", pstBkInfo->strCtrlName.c_str(), pstBkInfo->bkColor, pstBkInfo->bkImage); 29 } 30 31 public: 32 CObserverManager() = default; 33 CObserverManager(CObserverManager&) = delete; 34 CObserverManager& operator=(const CObserverManager&) = delete; 35 virtual ~CObserverManager() = default; 36 }; 37 38 //========================================================================== 39 //接收者实例1 40 class CReceiverButton : public CReceiverImpl<bool, PST_BKINFO> 41 { 42 public: 43 virtual bool Receive(PST_BKINFO pstBkinfo) override 44 { 45 printf("Receive information: Button\n"); 46 47 return true; 48 } 49 50 virtual bool Respond(PST_BKINFO pstBkinfo, IObserverImplBase<bool, PST_BKINFO>* pobjObserver) override 51 { 52 if ((nullptr == pstBkinfo) || (nullptr == pobjObserver)) return false; 53 CObserverManager* pobjObserverManager = dynamic_cast<CObserverManager*>(pobjObserver); 54 55 pstBkinfo->bkColor = 10; 56 pstBkinfo->bkImage = 20; 57 pstBkinfo->strCtrlName = "Button"; 58 pobjObserverManager->GetReceiverInfo(pstBkinfo); 59 60 return true; 61 } 62 63 public: 64 CReceiverButton() 65 { 66 CObserverManager::GetInstance().AddReceiver(this); 67 } 68 virtual ~CReceiverButton() = default; 69 }; 70 71 //接收者实例2 72 class CReceiverRidio : public CReceiverImpl<bool, PST_BKINFO> 73 { 74 public: 75 virtual bool Receive(PST_BKINFO pstBkinfo) override 76 { 77 printf("Receive information: Ridio\n"); 78 79 return true; 80 } 81 82 virtual bool Respond(PST_BKINFO pstBkinfo, IObserverImplBase<bool, PST_BKINFO>* pobjObserver) override 83 { 84 if ((nullptr == pstBkinfo) || (nullptr == pobjObserver)) return false; 85 CObserverManager* pobjObserverManager = dynamic_cast<CObserverManager*>(pobjObserver); 86 87 pstBkinfo->bkColor = 40; 88 pstBkinfo->bkImage = 80; 89 pstBkinfo->strCtrlName = "Ridio"; 90 pobjObserverManager->GetReceiverInfo(pstBkinfo); 91 92 return true; 93 } 94 95 public: 96 CReceiverRidio() 97 { 98 CObserverManager::GetInstance().AddReceiver(this); 99 } 100 virtual ~CReceiverRidio() = default; 101 }; 102 103 int _tmain(int argc, _TCHAR* argv[]) 104 { 105 std::unique_ptr<ST_BKINFO> pstBkinfo = std::make_unique<ST_BKINFO>(); 106 107 //各种控件 108 CReceiverButton objReceiverButton; 109 CReceiverRidio objReceiverRidio; 110 111 //观察者“广播”或“通知”消息 112 CObserverManager::GetInstance().Broadcast(pstBkinfo.get()); 113 CObserverManager::GetInstance().Notify(pstBkinfo.get()); 114 115 return 0; 116 }
这个”观察者模式“例子是以前使用"Duilib"时从源码中看到过的,用来生成”菜单“按钮和实现”换肤"功能。这里单独抽离出来,没有进行很复杂的功能展示,只是觉得它使用的知识点比较多,适合放在这里。
-
- 类模板的总结:优点--》1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生;2. 增强了代码的灵活性,简化编程工作,提高程序的可靠性。
缺点--》1. 模板会导致代码膨胀问题,也会导致编译时间变长; 2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
10. C++ 11标准
其实在讲前面的内容时,很多地方都使用了C++11的新特性,有的详细展开过,有的则是给了相应的链接资料。相对于C++98,C++11可以算一个比较大的版本更新,甚至在某些领域演变成一种全新的编程方式(eg.元编程)。可惜,这一块使用和认识的也不多,没办法深入探讨了,有兴趣的建议看看《effective STL》,如果还想再深入些,建议好好学习下boost库,C++11标准里的很多更新内容都是从里面提取出来的,再到后面的C++14,C++17,C++20就可以根据需求去学习了。