一.绪论:
1.1 C++简史:
与C的关系:
被设计为C语言的继任者,C语言是一种过程型语言,程序员使用它定义执行特定操作的函数,而C++是一种面向对象的语言,实现了继承、抽象、多态和封装等概念。C++支持类,而类包含成员数据及操作数的成员方法(方法类似于C语言的函数)。
C++优点:
C++是一种中级变成语言,这意味着使用它既可以高级编程方式编写应用程序,又可以低级变成方式编写与硬件紧密协作的库。在很多程序员看来,C++既是一种高级语言,让他们能够开发复杂的应用程序,又提供了极大的灵活性,让开发人员能够控制资源的使用和可用性,从而最大限度地提高性能。
1.2 编写C++应用程序 :
生成可执行文件的步骤:
C++代码(通常包含在.CPP文本文件中)被转换为处理器能够处理的字节码。编译器每次转换一个代码文件,生成一个扩展名为.o或.obj的目标文件,并忽略这个CPP文件可能对其他文件中代码的依赖。解析这些依存关系的工作由链接程序负责。除将各种目标文件组合起来外,链接程序还建立依存关系,如链接成功,则创建一个可执行文件,供程序员执行和分发。
C++11新增的功能:
auto能够定义这样的变量,即编译器将自动推断其类型,这简化变量声明,同时又不影响类型安全。Lambda函数是没有名称的函数,能够编写紧凑的函数对象,而无需提供冗长的类定义,从而极大地减少了代码。C++11让程序能够编写可移植的多线程C++应用程序,同时确保它们遵守标准。这些程序支持并行执行范式,在用户升级到多核CPU以改善硬件配置时,其性能将相应提升。
二、C++程序组成部分:
C++程序由类、函数、变量及其他元素组成。
预处理器编译指令#include:
预处理器编译指令时向预处理器发出命令,总是以符号#打头。尖括号(<>)通常包含标准头文件(#include<iostream>)。
名称空间概念:
为避免添加限定符,可使用using namespace。
#include "test.h"
#include <iostream>
int test::main1() {
using namespace std;
cout << "Hello World"<< endl;
return 0;
}
int test::main2() {
std::cout << "Hello World" << std::endl;
return 0;
}
int test::main3() {
using std::cout;
using std::endl;
cout << "Hello World" << endl;
return 0;
}
C++函数:
C++函数与C语言函数相同。函数可将应用程序划分多个功能单元,并按选择顺序调用。函数被调用时,通常将一个值返回调用它的函数。
三、使用变量和常量:
变量能够将数据临时存储一段时间,而常量能够定义不允许修改的东西。
编译器支持的常见C++变量类型:
使用sizeof确定变量的长度:
变量长度指的是,声明变量时,编译器将预留多少内存,用于存储赋给该变量的数据。变量的长度随类型而异。
使用typedef替换变量类型:
typedef unsigned int STRICTY_POSITIVE_INTEGER;
int test::main1() {
STRICTY_POSITIVE_INTEGER positiveInteger = 55555;
return 0;
}
常量:
常量类似于变量,只是不能修改。与变量一样,常量也是占用内存空间,并使用名称标识为其预留空间的地址,但是不能覆盖该空间的内容。
- 字面常量;
- 使用关键字const声明的常量;
- 使用关键字constexpr声明的常量表达式(C++11新增的);
- 使用关键enum声明的枚举常量;
- 使用#defind定义的常量(已屏蔽,不推荐)。
四、管理数组和字符串:
什么是数组:
- 数组是以系列元素;
- 数据中所有元素的类型都相同;
- 这组元素形成一个完整的集合。
动态数组:
#include <vector>
int test::main1() {
std::vector<int> dynArr(3);
dynArr[0] =365;
dynArr[1] =36;
dynArr[2] =35;
dynArr.push_back(20);
return 0;
}
五、使用表达式、语句和运算:
从本质上说,程序是一组按顺序执行的命令。这些命令的表达式和语句,使用运算符执行特定的计算或操作。
使用运算符:
运算福是C++提供的工具,能够使用数据对其进行变换、处理甚至根据数据做决策。
- 赋值运算符(=),左值通常是内存单元,右值可以使内存单元的内容;
- 加法运算符(+)、减法运算符(-)、乘法运算符(*)、除法运算符(/)和求模运算符(%);
- 递增运算符(++)和递减运算符(--):放在操作数时称为前缀递增或递减运算符;而放在操作数时,称为后缀递增或递减运算符。
- 相等运算符(==)和不等运算符(!=);
- 关系运算符:
- 逻辑运算符NOT、AND、OR、XOR:逻辑NOT运算用运算符!表示,用于单个操作数,将提供的布尔标记反转;逻辑AND运算用运算符&&表示,仅当两个操作数都为true时结果才为true;逻辑OR运算用||符号表示,只要有一个操作数为true,则结果为true;逻辑XOR(异或)有且只有一个操作数为true时,结果才为true,符号为^表示。
- 按位运算符NOT(~)、AND(&)、OR(|)、XOR(^):逻辑运算符和按位运算符之间的差别在于,按位运算符返回的并非布尔值,而是对操作数对应位执行指定运算的结果。
- 按位右移运算符(>>)和左移运算符(<<):移位运算符将整个位序列向左或向右移动,其用途之一是将数据乘以或除以2的N次方;
- 复合赋值运算符:
- 运算符sizeof确定变量占用的内存量:指出特定类型或变量的内存量,单位位字节。sizeof(variable)或sizeof(type)。
- 运算符的优先级:
六、控制程序流程:
- if...else条件控制;
- switch-case条件控制;
- 三目运算符?:条件控制;
- 不成熟的goto循环:将指令指针移到代码的特定位置,使用goto回过头再去执行特定语句。不推荐使用goto。
void test::main3() {
JumpToPoint:
std::vector<int> dynArr(3);
dynArr[0] =365;
dynArr[1] =36;
dynArr[2] =35;
dynArr.push_back(20);
goto JumpToPoint;
}
- while循环;
- do...while循环:需要将代码放在循环中,并确保至少执行一次;
- for循环;
- continue和break修改循环的行为。
七、使用函数组织代码:
1.函数原型是什么:
函数原型指出了函数的名称、函数接受的参数列表以及返回值的类型。
函数调用和实参:
函数声明中包含形参,调用函数时必须提供实参。
带默认值的函数参数:
参数包含默认值的函数,这种默认值可被用户提供的值覆盖。可以给出多个参数指定默认值,但这些参数必须位于参数列表的末尾。
递归函数——调用自己的函数:
递归函数必须有明确的退出条件,满足这种条件后,函数将返回,而不再调用自己。
2. 使用函数处理不同类型的数据:
并非只能每次给函数传递一个值,还可将数组传递给函数。可创建多个名称和返回值类型相同,但参数不同的函数。也可创建其参数不是在函数内部创建和销毁的;为此可使用在函数退出后还可用的引用,这样搞可在函数中操纵更多数据或参数。
函数重载:
名称和返回类型相同,但参数不同的函数被称为重载函数。
- 将数组传递给函数;
- 按引用传递参数;
微处理如何处理函数调用:
函数调用意味着微处理器跳转到属于被调用函数的下一条指令处执行。执行完函数的指令后,将返回到最初离开的地方。为实现这种逻辑,编译器将函数调用转换为一条供微处理器执行CALL指令,该指令指出了接下来要获取的指令所在的地址,该地址归函数所有。编译函数本身时,编译器将return语句转换为一条供微处理器的RET指令。
遇到CALL指令时,微处理器将调用函数后将执行的指令的位置保存到栈中,再跳转到CALL指令包含的内存单元处。
该内存单元包含属于函数的指令。微处理器执行它们,直到到达RET语句。 RET语句导致微处理器从栈中弹出执行CALL指令时存储的地址。该地址包含调用函数中接下来要执行的雨具的位置。这样微处理器将返回到调用函数,从离开的地方继续执行。
内联函数:
常规函数调用转换为CALL指令,这会导致栈操作、微处理器跳转到函数处执行等。而inline函数被调用时就地展开。将函数声明为内联的会导致代码急剧膨胀,在声明为内联的函数做了大量复杂处理时尤其如此。应尽可能少用关键字inline,仅当函数非常简单,需要降低其开销时,才使用该关键字。
lambda函数:
八、阐述指针和引用:
指针:
指针是存储内存地址的变量,与所有变量一样,指针也占用内存空间。指针的特殊之处在于,指针包含的值被解读为内存地址,因此指针是一种指向内存单元的特殊值。
声明指针:
作为一种变量,指针也需声明,通常将指针声明为指定特定类型;也可将指针声明为指向一个内存块,这种指针被称为void指针。
使用引用运算符(&)获取变量的地址:
要将变量的地址存储到一个指针中,需要声明一个同样类型的指针,并使用引用运算符(&)将其初始化为该变量的地址。
使用解除引用运算符(*)访问指向的数据:
使用解除引用运算符(*)用于指针时,应用程序从它存储的地址开始,取回内存中4个字节的内容,因此指针包含的地址必须合法。
将sizeof用于指针的结果
指针时包含内存地址的变量,因此无论指针指向哪种类型变量,其内容都是一个地址——一个数字。在特定的系统中,存储地址所需的字节是固定的。因此,将sizeof用于指针时,结果取决于编译程序时使用的编译器和针对的操作系统,与指针指向的变量类型无关。
动态内存分配:
要编写根据用户需要使用内存资源的应用程序,需要使用动态内存分配。能够根据需要分配更多内存,并释放多余的内存。为帮助更好的管理应用程序占用的内存,C++提供了两个运算符——new和delete。指针是包含内存地址的变量,在高效地动态分配内存方面扮演了重要角色。
使用new和delete动态地分配和释放内存:
使用new来分配新的内存块。如成功,new将返回指向一个指针,指向分配的内存,否则将引发异常。使用new时,需要指定要为哪种数据类型分配内存;使用new分配的内存最终都需要使用对应的delete进行释放。对于使用new[...]分配的内存块,需要使用delete[]来释放。
int* pointer = new int[10];
delete pointer;
将递增和递减运算符(++和--)用于指针的结果:
将指针递增或递减时,其包含的地址将增加或减少指向的数据类型的sizeof(并不是一定1字节)。这样,编译器将确保指针不会指向数据的中间或末尾,而只会指向数据的开头。
将关键字const用于指针:
通过将变量声明为const的,可确保变量的取值在整个生命周期内都是固定位初始值。这种变量的值不能修改,因此不能将其用作左值。
- 指针指向的数据为常量,不能修改,但可以修改指针包含的地址,即指针可以指向其他地方。
int housInDay = 24;
const int* pInt = &housInDay; //不能使用pInt去改变
int monthsInyear = 12;
pInt = &monthsInyear; //可以修改包含的地址
*pInt = 13; //编译失败,不能改变数据
int * pInt1 = pInt; //编译失败,不能指定const
- 指针包含的地址是常量,不能修改,但可以修改指针指向的数据:
int dayInMonth =30;
int* const pDayInMonth = &dayInMonth;
*pDayInMonth = 31; //可以修改指向的数据
int dayIn2Month = 28;
pDayInMonth = &dayIn2Month; //不能该指向的地址
指针包含的地址以及它指向的值都是常量,不能修改:
int hourInDay = 24;
const int* const pHourInDay = &hourInDay; //指针仅仅指向hourInDay
*pHourInDay = 25; //编译失败,不能修改指向的数据
int dayInDay = 25;
pHourInDay = &dayInDay; //编译失败,不能修改指针指向的地址
将指针传递给函数:
指针是一种将内存空间传递给函数的有效方式,其中可以包含值,也可以包含结果。将指针作为函数参数时,确保函数只能修改要修改的参数很重要。
使用指针时时常犯的编程错误:
C++能够动态地分配内存,以优化应用程序对内存使用。不同于Java和C#等基于运行时环境的新语言,C++没有自动垃圾收集器对程序已分配但不能使用的内存进行清理。
- 内存泄漏:如在使用new动态分配的内存不再需要后,没有使用配套的delete释放;
- 指针向无效的内存单元:使用运算符*对指针解除引用,以访问指向的值时,务必确保指针指向了有效的内存单元,否则程序要么崩溃,要么行为不端。
- 悬浮指针(也叫迷途或失控指针):使用delete释放后,任何有效指针都将无效。为避免这种问题,在初始化指针或释放指针后将其设置为NULL,并在使用运算符*对指针解除引用前检查它是否有效。
指针编程最佳实践:
检查使用new发出的分配请求是否得到满足:
除非请求分配的内存量特大,或系统处于临界状态,可供使用的内存很少,new一般都能成功。有些应用程序需要请求分配大块的内存(如数据库应用程序),一般而言,不要假定内存分配能成功,这很重要。C++提供了两种确保指针有效的方法,默认方法是使用异常,即如内存分配失败,将引发std::bad_alloc异常。这导致应用程序中断执行,除非提供了异常处理程序,否则应用程序将崩溃,并显示一条类似于“异常未处理”的消息。
有一种new变种——new(newthrow),它不引发异常,而返回NULL,能够在使用指针检查其有效性:
int* pAge = new(std::nothrow)int[0x1fffffff];
if (pAge)
{
delete[] pAge;
} else
{
}
引用是什么
引用是变量的别名。声明引用时,需要将其初始化为一个变量,因此引用只是另一种访问相应变量存储的数据的方式。
引用能够访问相应变量所在的内存单元,
Returntype doSomething(Type Parameter);
//会将argumnet赋值给Parameter,再被doSomething()调用
// ,如argumnet占用大量内存,这个复制过程的开销很大
Returntype returntype = doSomething(argumnet);
Returntype doSomething(&Type Parameter);
//由于argumnet1是按引用传递的,Parameter不再是argumnet的拷贝,而是它的别名
Returntype returntype = doSomething(argumnet1);
将关键字const用于引用:
可能需要禁止通过引用修改它指向的变量的值,为此 可在声明引用时使用关键字const。
int origin = 30;
const int& contRef = origin;
contRef = 40; //不被允许 引用contRef修改origin的值
int& ref2 = contRef; //不被允许,ref2没有被const修饰
const int& ref3 = contRef;
按引用向函数传递参数:
引用的有点之一是,可避免将形参复制给形参,从而极大地提高性能。然而,让被调用的函数直接使用调用函数栈时,确保被调用不能修改调用函数中的变量很重要。为此,可将引用声明为const的。
九、类和对象:
类、成员属性、函数访问:
- 声明类;可使用关键字class,并在它后面一次包含类名、一组放在{}内的成员属性和方法以及结尾的分号;
- 实例化对象:要使用类的功能,通常需要根据类实例化一个对象,并通过对象访问成员方法和属性;
- 使用句点运算符访问成员;
- 使用指针运算符(->)访问成员:如对象是使用new在自由存储区中实例化,或有指向对象的指针,则可使用指针运算符(->)来访问成员属性和方法。
关键字public和private;
构造函数:
构造函数是一种特殊的函数(方法),在创建时被调用。与函数一样,构造函数也可以重载。
- 声明和实现构造函数:构造函数是一种特殊的函数,它与类名同名且不返回任何值;
- 重载构造函数;
- 没有默认构造函数的类;
- 带默认值的构造函数参数:
- 包含初始化列表的构造函数;
析构函数:
与构造函数一样,析构函数也是一种特殊的函数。与构造函数不同的是,析构函数在对象销毁时被自动调用。
- 声明和实现析构函数:与类同名的函数,但前面有一个波浪号(~)。
- 何时及如何使用析构函数:每当对象不再作用域内或通过delete被删除,进而被销毁,都将调用析构函数。这使得析构函数是重置变量以及释放动态分配的内存和其他资源的理想场所;
- 析构函数不能重载,每个类都只能有一个析构函数,如未实现析构函数,编译器将创建一个伪(dummy)析构函数并调用它,伪析构函数为空,即不释放动态分配的内存。
复制构造函数:
- 浅复制及其存在的问题:在一个类中包含一个指针成员,它指向动态分配的内存(这些内存是在构造函数中使用new分配的,并在析构函数中使用delete[]进行释放)。复制这个类的对象时,将复制其指针成员,但不复制指针指向的缓冲区,其结果是,两个对象指向一块动态分配的内存。这被称为浅复制,会威胁程序的稳定性;
#include "MyString.h"
#include <iostream>
using namespace std;
MyString::MyString(const char *initInput) {
cout << "调用构造函数"<< endl;
if (initInput != NULL) {
buffer = new char[strlen(initInput)+1];
strcpy(buffer,initInput);
} else {
buffer = NULL;
}
}
MyString::~MyString() {
cout << "调用析构函数,清除"<< endl;
if (buffer != NULL)
delete[] buffer;
}
int MyString::getLength() {
return strlen(buffer);
}
const char *MyString::getString() {
return buffer;
}
//二进制复刻并不是深复制指向的内存单元,导致MyString对象指向同一个内存单元。
void MyString::useMyString(MyString input) {
cout << "调用浅复刻函数"<< endl;
cout << "Sting buffer:" << input.getLength();
cout << "获取String"<< input.getString()<<endl;
return;
}
- 使用复制构造函数确保深复刻:复制构造函数一个特殊的重载构造函数,编写类必须提供,当对象被复制(包括对象按值传递给函数)时,编译器都将调用复制构造函数;复制构造函数接收一个以引用方式传入的当前类的对象作为参数,这个参数时源对象的别名,使用它来编写自定义的复制代码,确保对所有缓冲区进行深复刻。类包含原始指针成员时,务必编写复制构造函数和复制复制运算符;编写复制函数时,务必接收源对象的参数声明为const引用。除非万不得已,不要类成员声明为原始指针。
#include "MyString.h"
#include <iostream>
using namespace std;
//在复刻构造函数声明中使用const,可确保复制构造函数不会修改指向的源对象
//另外,复制构造函数的参数必须按引用传递,否则调用它时复制实参的值,导致源对象
//进行浅复制。
void MyString::useMyString(const MyString &intput) {
cout << "调用深复刻函数"<< endl;
cout << "Sting buffer:" << intput.buffer;
return;
}
有助于改善性能的移动构造函数:
//为了性能瓶颈,还可编写移动构造函数。有移动构造函数时,
//C++编译器将自动使用它的“移动”临时资源
//从而避免深复制,移动构造函数通常时利用移动赋值运算符实现的。
void MyString::useMyString(MyString &&intput) {
return;
}
构造函数和析构函数的其他用途:
-
不允许复制的类:要禁止对象被复制,可声明一个私有的复制构造函数。这确保函数调无法通过编译。为禁止赋值,可声明一个私有的赋值运算符。
- 只能有一个实例的单例类:使用私有构造函数、私有复制运算符和静态成员。
将关键字static用于类的数据成员时,该数据成员将在所有实例之间共享;
将static用于函数声明的局部变量时,该变量的值将在两次调用之间保存不变;
将static用于成员函数时,该方法将在所有成员之间共享。
- 禁止在栈中实例化的类:关键在于将析构函数声明为私有。
this指针:
在类中,关键字this包含当前对象的地址,换句话说,其值为&object。当在类成员方法中调用其他成员方法时,编译器将隐式地传递this指针——函数调用中不可见的参数。调用静态方法时,不会隐式传递this指针,因静态函数不与类实例相关联,而由所有实例共享。如要静态函数中使用实例变量,应显示地声明一个形参,让调用者将实参设置为this指针。
将sizeof()用于类:
通过使用关键字class声明自定义类型,可封装数据属性和使用数据的方法。sizeof()可用于类声明中所有数据属性占用的总的内存量。sizeof不考虑成员函数及其定义的局部变量。
友元类及函数:
不能从外部访问类的私有数据成员和方法,但这条规则不适用于友元类和友元函数。要声明友元类或友元函数,可使用关键字friend。
十、实现继承:
C++入门知识点总结——面向对象/高级编程
十一、多态:
C++入门知识点总结——面向对象/高级编程
使用虚函数实现多态行为:
通过使用Virual,可确保编译器调用覆盖版本。
- 虚函数的工作原理——理解虚函数表:编译器将为实现了虚函数的基类和覆盖了虚函数的派生类分别创建一个虚函数表(Virtual Function Table,VFT)。实例化类的对象时,将创建一个隐藏的指针(我们称之为VFT*),它指向相应的VFT。可将VFT视为一个包含函数指针的静态数组,其中每个指针都指向相应的虚函数。每个虚函数表都由函数指针组成,其中每个指针都指向相应虚函数的实现。
- 抽象基类和纯虚函数:不能实例化的基类被称为抽象基类,这样的基类只有一个用途,那就是从它派生出其他类。在C++中,要创建抽象基类,可声明纯虚函数。
- 虚继承解决二义性:在继承层次结构中,继承多个从同一类派生而来的基类时,如这些基类没有采用虚继承,将导致二义性;在继承层次结构中使用关键字virtual,将基类的实例个数限定为1;
对于将派生类覆盖的基类方法,务必将其声明为虚函数;纯虚函数导致类变成抽象基类,且在派生类中必须提供纯虚函数的实现;务必考虑使用虚继承;
别忘了给基类提供一个虚析构函数;别忘了编译器不允许创建抽象基类的实现,别忘了在二义性继承层次结构中,虚继承旨在确保只有一个基类实现;用于创建继承层次结构和声明基类函数时,关键字virtual的作用不同。
十二、运算符类型和运算符重载:
函数运算符operator()
operator()让对象像函数,被称为函数运算符。函数运算符用于标准模板库(STL)中,通常时STL算法中。其用途包括决策。根据使用的操作数数量,这样的函数对象称为单目谓词或双目谓词。
十三、类型转换运算符:
类型转换是一种机制,能够暂时或永久性改变编译器对对象的解释。注意,这并意味着程序改变了对象本身,而是改变了对对象的解释。可改变对象解释方式的运算符称为类型转换运算符。
- static_cast:用于在相关类型的指针之间进行转换,还可显式地执行标准数据类型的类型转换——这种转换原本将自动或隐式地进行。static_cast实现了基本的编译阶段检查,确保指针被转换为相关类型。使用static_cast可将指针向上转换为基类类型,也可向下转换为派生类型。
- dynamic_cast:与静态类型转换相反,动态类型转换在运行阶段(即应用程序运行时)执行类型转换。可检查dynamic_cast操作的结果,以判断类型转换是否成功。
- reinterpret_cast:能够将一种对象类型转换为另一种,不管它们是否相关;这种类型转换实际上是强制编译器接受static_cast通常不允许的类型转换,通常用于低级程序(如驱动程序),在这种程序中,需要将数据转换成API能够接受的简单类型。
- const_cast:能够关闭对象的访问修饰符const。
十四、宏和模板简介:
预处理与编译器:
预处理器在编译之前运行,预编译器指令都以#打头。C++程序通常在.h(头文件)中声明类和函数,并在.cpp文件中定义函数,因此需要在.cpp文件中使用预处理器编译指令#include<header>来包含头文件。
- #define定义常量;
- #ifndef和#endif:在预处理器看来,两个头文件包含对方会导致递归问题。为避免这种问题,可结合使用过宏以及预处理器编译指令#ifndef和#endif;
- assert:使用assert宏验证表达式;
模板:
在C++中,模板能够定义一种适用于不同类型的对象的行为;宏不是类型安全的,而模板是类型安全的。模板声明以关键字template打头,接下来是类型参数列表。
#include <iostream>
#include <string>
using namespace std;
template <typename T>
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}
int main ()
{
int i = 39;
int j = 20;
cout << "Max(i, j): " << Max(i, j) << endl;
double f1 = 13.5;
double f2 = 20.7;
cout << "Max(f1, f2): " << Max(f1, f2) << endl;
string s1 = "Hello";
string s2 = "World";
cout << "Max(s1, s2): " << Max(s1, s2) << endl;
return 0;
}
- 模板函数:
模板函数不仅可以重用(就像宏函数一样),而且更容易编写和维护,还是类型安全的。
- 模板类:使用模板类时,可指定要为哪种类型具体化类。
template <typename T>
class MyFristTemplateCalss {
public:
void setValue(const T& newValue) {
vaule = newValue;
}
const T& getValue() const {
return vaule;
}
private:
T vaule;
};
- 使用static_assert执行编译阶段检查:static_assert是C++新增的一项功能,能够在不满足指定条件时禁止编译,它是一种编译阶段断言,可用于在开发环境(或控制台中)显示一条自定义消息。
void EverythingButInt(){
static_assert(sizeof(T) != sizeof(int),"NO Int please!");
};
- 在实际C++编程中使用模板:模板最重要也是最强大的应用是在标准模板库(STL)中。STL由一系列模板类和函数组成,分别包含泛型使用类和算法。这些STL模板类能够实现动态数组、链表以及包含键-值对的容器,而sort等算法可用于这些容器,从而对容器包含的数据进行处理。
十五、标准模板库简介:
STL容器:
容器是用于存储数据的STL类,STL提供了两种类型的容器:顺序容器和关联容器,还提供了容器适配器(Contsiner Adapter)的类,是顺序容器和关联容器的变种,包含的功能有限,用于满足特殊的需求。
顺序容器:
顺序容器按顺序存储数据,如数组和列表。顺序容器具有插入速度快但查找操作相对较慢的特征。
- std::vector:操作与动态数组一样,在最后插入数据;可将vector视为书架,可在一端添加和拿走是图书;
- std::deque:与std::vector类似,但允许在开头插入或删除元素;
- std::list:操作与双向链表一样。可将它视为链条,对象被连接在一起,可在任何位置添加或删除对象;
- std::forward_lis:类似于std::list,但是单向链表,只能一个方向遍历。
关联容器:
关联容器按指定的顺序存储数据,就像词典一样。这将降低插入数据的速度,但在查询方面有很大的优势。
- std::set:存储各不相同的值,在插入时进行排序;容器的复杂度为对数;
- std::unordered_set:存储各不相同的值,在插入时进行排序;容器的复杂度为常数。这是容器是C++新增的;
- std::map :存储键值对,并根据唯一的键排序;容器的复杂度为对数;
- std::unordered_map:存储键值对,并根据唯一的键排序;容器的复杂度为对数,这是容器是C++新增的;
- std::multiset:与set类似,但允许存储多个值相同的项,即值不需要是唯一的;
- std::unordered_multiset:与unordered_set类似,但允许存储多个值相同的项,即值不需要是唯一的,这是容器是C++新增的;
- std::multimap:与map类似,但不要求键是唯一的;
- std::unordered_multimap:与unordered_map类似,但不要求键是唯一的,这是容器是C++新增的;
选择正确的容器:
容器适配器:
-
std::stack:以LIFO(后进先出)的方式存储元素 ,能够在栈顶插入(压入)和删除(弹出)元素;
-
std::queue:以FIFO(先进先出)方式存储元素,能够在栈顶插入和删除元素;
-
std::priority_queue:以特定顺序存储元素,因为优先级最高的元素总工室位于队列开头。
STL迭代器:
最简单的迭代器是指针。给定一个指向数组的第一元素的指针,可递增该指针使其指向下一个元素,还可直接对当前位置的元素进行操作。
STL中的迭代器是模板类,从某种程度上说,它是泛型指针。这些模板类能够对STL容器进行操作,注意,操作也可以以模板函数的方式提供STL算法,迭代器是一座桥梁,让这些模板函数能够以一致而无缝的方式处理容器,而容器是模板类;
- 输入迭代器:通过对输入迭代器解除引用,它将引用对象,而对象可能位于集合中。最严格的输入迭代器确保只能以只读的方式访问对象;
- 输出迭代器:输出迭代器能对集合执行写入操作。最严格的输出迭代器确保只能执行写入操作;
- 前向迭代器:这是输入迭代器和输入迭代器的一种细化,它允许输入和输出。前向迭代器可以使const的,只能读取它指向的对象;也可以改变对象,即可读写对象。前向迭代器通常用于单链表;
- 双向迭代器:这是前向迭代器的一种细化,可对执行递减,从而向后移动。双向迭代器通常用于双向链表;
- 随机访问迭代器:这是对双向迭代器的一种细化,可将其加减一个偏移量,还可将两个跌大气相减以得到集合中两个元素的相对距离。随机访问迭代器通常用于数据。
STL算法:
- std::find:在集合中查找值。
- std:find_if:根据用户指定的谓词在集合中查找值;
- std::reverse:反转集合中元素的排列顺序;
- std::remove_if:根据用户定义的谓词将元素从几个中删除;
- std::trasnsform:使用用户定义的变换函数对容器中的元素进行变换。
STL字符串类:
- std::string:基于插入的std:basic_string具体化,用于操纵简单字符串;
- std:wstring:基于wchar_t的std:basic_string具体化,用于操纵宽字符串。
十六、STL String类:
标准模板库(STL)提供了一个用于字符串操作的容器类。string类不仅能够根据应用程序的需求动态调整大小,还提供了很有用的辅助函数,可帮助操作字符串,能够在应用程序中使用标准的、经过测试的可移植功能,并将其主要精力放在开发应用程序的重要功能上。
STL字符串类std::string 和std::wstring
- 减少了程序员在创建和操作字符串方面需要做的工作;
- 在内部管理内存分配细节,从而提高了应用程序的稳定性;
- 提供了复制构造函数和赋值运算符,可确保成员字符串得以正确复制;
- 提供了帮助执行复制、截短、删除等操作的实用函数;
- 提供了帮助用于比较的运算符;
- 让程序员能够将精力放在应用程序的主要需求而不是字符串操作细节上。
实例化和复制STL string;
访问std::sting的字符内容:
拼接字符串;
在string中查找字符串或子字符串:
截短STL string:
字符串反转:
字符串的大小写转换:
基于模板的STL string实现:
std::string类实际上是STL模板类std::basic_string <T>具体化。
十七、STL 动态数组类:
std::verctor的特点:
- 在数组末尾添加元素所需的时间是固定的,即在末尾插入元素的所需时间不随数组大小而异,在末尾删除元素也如此;
- 在数组中间添加或删除元素所需的时间与该元素后面的元素个数成正比;
- 存储的元素数是动态的,而vector类负责管理内存。
实例化vector:
使用push_back在末尾插入元素:
使用insert()在指定位置插入元素:
使用数组语法访问vector中元素:
使用指针语法访问vector中的元素:
删除vector中的元素:
STL deque类:
deque是一个STL动态数组类,与vector非常类似。但支持在数组开头和末尾插入或删除元素。
支持使用方法push_back()和pop_back()末尾插入和删除元素,还允许使用push_front和pop_front在开头插入和删除元素。
十八、STL list 和 forward_list:
标准模板类(STL)以模板类std::list的方式提供了一个双向链表。双向链表的主要特点是,插入和删除元素的速度快,且时间是固定。从C++11起,还可使用单向链表std::forwead_list,这种链表只能沿一个方向遍历。
std::list的特点:
链表是一系列节点,其中每个节点除包含对象或值外还指向下一个节点,即每个节点都连接到下一个节点和前一个节点。
基本的list操作:
- 实例化vector::list对象;
- 在list开头或末尾插入元素:push_front()和push_back();
- 在list中间插入元素:
- 删除list中元素:
- 对包含对象的list进行排序以及删除其中的元素:
对list中元素进行反转和排序:
- 使用list::reverse()反转元素的排列顺序:
- 对元素进行排序:
std::forward_list:
单向链表std::forwead_list,这种链表只能沿一个方向遍历。
十九、STL 集合类:
标准模板库提供了一些容器类,以便在应用程序中进行频繁而快速的搜索。std::set和std::multiset用于存储一组经过排序的元素,其查找元素的复杂度为对数,而unordered集合的插入和查找时间是固定的。
STL set和multiset的基本操作:
- 实例化std::set对象:
- 在set或multiset中插入元素:
- 在set或multiset中查找元素:
- 删除set或multiset中的元素:
STL散列集合实现std::unordered_set和std::unordered_multiset
set和multiset使用了std::less<T>或提供的谓词对元素(同时也是键)进行排序。相对与vector等未经排序的容器,在经过排序的容器中查找的速度更快,其sort的复杂度为对数。这意味着在set中查找元素时,所需的时间不是与元素数成正比,而是与元素数的对数成正比。
相比于未经排序的容器(查找时间与元素数成正比),这极大地改善了性能,但有时候这还不够。探索出插入和排序时间固定的方式,一种这样的方式是使用基于散列的实现,即使用散列函数来计算排序索引。将元素插入散列集合时,首先使用散列函数计算出一个唯一的索引,在根据该索引决定将元素放在哪个桶(bucket)中。
二十、STL 映射类:
STL映射类简介:
map和multimap是键-值对容器,支持根据键进行查找。
map和multimap区别在于,后者能存储重复的键,而前者只能存储唯一的键。
std::map和std::multimap的基本操作:
- 实例化std::map和std::multimap:
- 在std::map和std::multimap中插入元素:
- 在std::map和std::multimap中查找元素:
- 删除std::map和std::multimap中的元素:
提供自定义的排序谓词:
std::map和std::multimap使用std::less<>提供的默认排序标准,该谓词<运算符比较两个对象。
散列表的工作原理:
可将散列表视为一个键-值对集合,根据给定的键,可找到相应的值。散列表与简单映射的区别在于,散列表将键值对存储在桶中,每个桶都有索引,指出了它的散列表中的相对位置(类似于数组)。这种索引使用散列函数根据键计算的得到的:
index = HashFunction(key,TableSzie);
使用find(0根据键查找到元素时,将使用HashFunction()计算元素的位置,并返回该位置的值,就像数组返回其存储的元素那样。如HashFunction()不佳,将导致多个元素的索引相同,进而存储在同一个桶中,即桶变成了元素裂表。这种情形被称为(collision),它降低查找速度,是查找时间不再固定。
std::unordered_map和std::unordered_multimap:
在不发生冲突的情况下unordered_map的插入和查找时间几乎是固定的,不受元素数的影响。然而,这并不意味着它优于在各种情形复杂度都为对数的map。在包含的元素不太多的情况下,固定时间可能长得多,导致unordered_map的速度比马屁慢。
二十一、 理解函数对象:
函数对象与谓词的概念:
函数对象是用做函数的对象;但从实现上说,函数对象是实现了operator()的类的对象。虽然函数和函数指针也可归为函数对象,但实现了operator()的类对象才能保存状态(即类的成员属性的值),才能用于标准模板库。
- 一元函数:接受一个参数的函数如f(x)。如一元函数返回一个布尔值,则该函数称为谓词;
- 二元函数:接受两个参数的函数如f(x,y)。如二元函数返回一个布尔值,则称该函数为二元谓词。
template <typename elementType>
struct displayElement{
void operator() (const elementType& element) const {
//doSomething
}
};
函数对象用途:
返回布尔值的一元函数时谓词,这种函数可供STL算法用于判断;接受连个参数并返回一个布尔值的函数时二元谓词。这种函数用于如std::sort()等STL函数中。
二十二、 C++ lambda表达式:
可将lambda表达式视为包含公有operator()的匿名结构(或类)。
如何定义lambda表达式:
lambda表达式的定义必须以方括号([])打头。这些括号告诉编译器,接下来是一个lambda表达式。方括号的后面是一个参数列表,该参数列表与不使用lambda表达式时提供给operator()的参数列表相同。
一元函数对应的lambda表达式:
[](Type paramname){ /// lambda表达式}
[](Type& paramname){ /// lambda表达式}
一元谓词对应的ambda表达式:
[](Type paramname){ // lambda表达式 return bool}
通过捕获列表接受状态变量的lambda表达式 :
[Divisor ](int dividend){ return (dividend % Divisor == 0; )}
lambda表达式的通用语法:
lambda表达式总是以方括号打头,并可接收多个状态变量,为此可在捕获列表([....])中指定这些状态,并用逗号分隔;
二元谓词对应的ambda表达式:
[](Type paramname2,Type paramname2){ /// lambda表达式}
一元谓词对应的ambda表达式:
[](Type paramname2,Type paramname2){ // lambda表达式 return bool}
二十三、 STL算法:
查找、搜索、删除和计数是一些通用算法,其应用范围很广。STL通过通用的模板函数提供了些算法以及其他的很多算法,可通过迭代器对容器进行操作。要使用STL算法,必须包含头文件<algoithm>;
非变序算法:
不改变容器中元素的顺序和内容的算法称为非变序算法。
变序算法:
变序算法改变其操作的序列的元素顺序或内容。
使用STL算法:
- 根据值或条件查找元素:
- 计算包含给定值或满足给定条件的元素数:
- 在集合中搜索元素或序列:
- 将容器中的元素初始为指定值:
- 使用std::generate()将元素设置为运行阶段生成的值:
- 使用for_each()处理指定范围内的元素:
- 使用std::transform()对范围进行变换:
- 复制和删除操作:
- 替换值以及替换满足给定条件的元素:
- 排序、在有序集合中搜索以及删除重复元素:
- 将范围分区:
- 在有序集合中插入元素:
二十四、 自适应容器:栈和队列
栈和队列的行为特征:
栈和队列与数组或list极其相似,但插入、访问和删除元素的方式有一定的限制。可将元素插入到什么位置以及可从什么位置删除元素决定了容器的行为特征。
栈:
栈是LIFO(后进后出)系统,只能从栈顶插入或删除元素。
队列:
队列是FIFO(先进先出)系统,元素被插入到队尾,最先插入的元素最先被删除。
使用STL stack类:
- 实例化stack:
- stack的成员函数:push、pop、empty、size、top;
- 使用push()和pop()在栈顶插入和删除元素:
使用STL queue:
- 实例化queue:
- queue的成员函数:push、pop、empty、size、front、back;
- 使用push()在队尾插入以及使用pop()从队首删除;
使用STL优先级队列:
- 实例化priority_queue类:
- priority_queue的成员函数:push、pop、top、empty、size;
- 使用push()在priority_queue末尾插入以及使用pop()在priority_queue开头删除;
二十五、使用STL位标志:
位是存储设置与标志的高效方法。
bitset类:
- 实例化std::bitset:
- 使用std::bitset及其成员:可用于在bitset中插入位、设置内容、读取内容;还提供了一些运算符,用于显示位序列、执行按位逻辑运算等;
- std::bitset的运算符:
- std::bitset的成员方法:
vector<bool>:
vector<bool> 是对std::verctor的部分具体化,用于存储布尔数据。这个类可动态地调整长度。
- 实例化vector<bool>:
- vector<bool> 的成员函数和运算符: