本文目录:
基础概念 关键字 数据类型 容器 运算符 指针 函数 类 模板 内存管理 链接库 代码规范
基础概念
简介
C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。一般被认为是一种中级语言,它综合了高级语言和低级语言的特点。
标准的 C++ 由三个重要部分组成:
- 核心语言,提供了所有构件块,包括变量、数据类型和常量,等等。
- C++ 标准库,提供了大量的函数,用于操作文件、字符串等。
- 标准模板库(STL),提供了大量的方法,用于操作数据结构等。
简单使用
C++ 程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象,方法、即时变量。参考
- 对象 - 对象具有状态和行为。例如:一只狗的状态 - 颜色、名称、品种,行为 - 摇动、叫唤、吃。对象是类的实例。
- 类 - 类可以定义为描述对象行为/状态的模板/蓝图。
- 方法 - 从基本上说,一个方法表示一种行为。一个类可以包含多个方法。可以在方法中写入逻辑、操作数据以及执行所有的动作。
- 即时变量 - 每个对象都有其独特的即时变量。对象的状态是由这些即时变量的值创建的。
运行:1.在文本编辑器里写好代码,保存为.cpp文件,打开命令提示符,进入到保存文件所在的目录。
2.输入 'g++ name.cpp ',输入回车,编译代码。如果代码中没有错误,命令提示符会跳到下一行,并生成 a.out 可执行文件。
3.输入 ' a.out' 来运行程序。
编译过程
预处理 –编译 –汇编 –链接
预处理主要对预处理指令做出处理,例如对 #define 的替换,插入 #include 头文件等操作 编译将预处理后的代码文件“翻译”成汇编语言的文件 链接就是把每个源代码独立编译后的结果,按照它们的要求将它们组装起来。链接主要解决的是源代码之间的相互依赖问题,生成可执行文件.exe
标识符和关键字
标识符:用来标识变量、函数、类、模块,或任何其他用户自定义项目的名称。一个标识符以字母 A-Z 或 a-z 或下划线 _ 开始,后跟零个或多个字母(区分大小写)、下划线和数字(0-9),标识符内不允许出现标点字符如 @、& 和 %等。
关键字:C++ 中的保留字。这些保留字不能作为常量名、变量名或其他标识符名称,部分如下:
using
// 引入命名空间
using namespace std;
// 指定别名
using a = b;
// 子类中引用基类的成员
class A {
public:
void a();
};
class B :public A {
public:
using A::a;
};
#include
-
预编译指令#include的作用是将所包含的文件全文复制到#include的位置,相当于是个展开为一个文件的宏。
-
C++允许多次声明,但只允许一次实现。比如int foo(); 就是一次声明,而int foo(){}就是一次实现。
-
如果编译时有多个.cpp 文件中#include 了同一个含有函数实现的.h,这时候链接器就会在多个目标文件中找到这个函数的实现,而这在C++中是不允许的,此时就会引爆 LNK1169 错误:找到一个或多个重定义的符号。
-
因此为了让函数可以在各个.cpp 中共享,正确的做法就是在.h中只声明函数,并在另一个.cpp 中实现这个函数。这样就不会冲突了。
#define宏定义
#define是预处理命令,像是简单的文本替换,发生在预处理阶段(编译器不进行正确性检验,最好用const代替)
1.1 基本功能:
#define MACRO_PRINTF printf //1.定义一个宏来替换字符串
#define MACRO_NAME //2.定义一个宏名称,但不写任何东西
#ifdef MACRO_NAME // if defined的缩写。然后通过预定义的开关来调整代码实现
#ifndef MACRO_NAME // if!defined的缩写。
#define MACRO_FUNC(x, y)
#endif
//3.可以定义类似函数的宏,但使用时可能有性能问题,不如真函数
1.2 参数的字符串化和拼接:
#define MACRO_STRING(x) #x //1.把一个宏变成一个字符串
//宏在碰到#的时候,后面的x就不会被展开。这样可以让任何输入的x都变成字符串
#define MACRO_EXPAND(x) MACRO_STRING(x) //为了让x展开,我们需要把宏嵌套一层
//通过MACRO_EXPAND 可以得到x展开之后的样子
#define MACRO_CAT(x, y) x##y //2.把宏输入的两个参数直接连接起来
#define MACRO_GLUE(x, y) MACRO_CAT(x, y) //3.让参数展开后再连接
1.3 变参宏
#define MACRO_ARGS(...) __VA_ARGS__
1.4 FILE、LINE、FUNCTION、…
#ifndef __GNUC__
#define __PRETTY_FUNCTION__ __FUNCTION__
#endif
#ifdef _DEBUG
#define MACRO_TRACE(fmt, ...)
printf("%s %s (%d) -> ", __FILE__, __PRETTY_FUNCTION__, __LINE__);
printf(fmt, ##__VA_ARGS__)
#else
#define MACRO_TRACE(fmt, ...)
#endif // _DEBUG
//编译器内置的宏定义:
//FILE:当前源文件名 LINE:当前源代码行号 FUNCTION:当前的函数名 DATE:当前的编译日期
//TIME:当前编译时间 STDC:当要求程序严格遵循ANSIC标准时该标识被赋值为1… …等等
typdef
在编译时被处理,作用是给已经存在的类型一个别名
typedef (int*) pINT;
#define pINT2 int*
//则pINT a,b; 效果同int *a; int *b;
//而pINT2 a,b; 效果同int *a; int b;
const常量
约束某个对象不能被修改,由编译器来实施约束。可以初始化数组 const在类外面可以用于全局和命名空间常量,以及静态变量(某一文件或程序块范围内的局部对象);在类内部可以用于静态与非静态成员;也可以和指针搭配,形成const指针和指向const的指针
const和define: 宏在预处理阶段,没数据类型,没安全检查,const在编译阶段,有数据类型,有安全检查 undef可以取消,const不能取消,define存储在代码段,不分配内存,const存储在数据段,分配内存
连接属性:extern const int a;可以加extern提高作用域;
对const常量取地址,会临时分配地址
constexpr
在编译时把常量表达式直接优化并植入到程序运行,增加程序的性能;
类似const,把表达式或函数编译为常量结果。但与const不同,constexpr也可以应用于函数和类构造函数。constexpr指示该值或返回值是恒定的,并在可能的情况下在编译时进行计算。
constexpr修饰的函数可以使用递归;
constexpr float x = exp(5, 3);
constexpr int max = 1 + 2 + 3;
constexpr float exp(float x, int n)
{
return n == 0 ? 1 :
n % 2 == 0 ? exp(x * x, n / 2) :
exp(x * x, (n - 1) / 2) * x;
};
//可以使用递归
constexpr int fibonacci(const int n)
{
return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}
常量表达式: constexpr int add(……){……} int a[add(2, 5)]={};将运行时的计算提前到编译时来优化性能,静态数组大小必须是常量,普通函数用在这里算变量会报错,但常量表达式ok。constexpr可以修饰任何表达式,从普通加减赋值,到递归函数,都合法。
static静态变量
修改存储区域,生命周期,作用范围等
1.面向过程中的static
——普通变量 修改变量的存储区域和生命周期,使变量存储在静态区。
可以在整个文件中共享,但不能被其他文件所用,其他文件有相同名称的变量也不会起冲突(与extern不同)
如果是静态局部变量,程序运行到该对象处时才初始化,如果未指定初始化值则初始化为默认值0
——普通函数 表明函数的作用范围,仅在声明它的文件里才能使用。在大型项目里可以防止与他人命名空间里的函数重名
2.面向对象中的static(类)
——成员变量 表示它是是属于类的,而不是属于某个对象实例,不需要生成对象就可以访问该成员
——成员函数 不需要生成对象就可以访问该函数,可以用 类名.方法名() 来调用,但该函数不能访问非静态成员,不能使用this(与实例相关)
inline内联函数
指嵌入式代码,可以提升效率,但需要消耗更多内存 编译时,编译器使用相应的函数代码替换函数调用,程序无需跳到另一个位置处执行代码再跳回来 虚函数可以是内联,仅当虚函数不表现多态性时
存储关键字extern
表明引用声明,即声明引用在其他地方定义的变量。
C++语言支持分离式编译,也就是说允许将程序分割为若干文件,每个文件都能独立编译。这样一个文件可能需要另一个文件中中定义的变量,所以C++允许将声明和定义分离开来。 变量的声明规定了变量的类型和名字,即使一个名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明 定义则负责创建与名字关联的实体,定义还申请存储空间 extern int i; //仅声明不定义 int j; //声明并定义 在类声明中定义的函数,虚函数除外,其他函数都会隐式地当成内联函数
存储关键字register
在声明中指示寄存器存储类型。用register声明的变量称为寄存器变量,在可能的情况下会直接存放在机器的寄存器中;但对32位编译器不起作用,当全局优化开的时候,它会做出选择是否放在自己的寄存器中;
存储关键字auto
它是存储类型标识符,表明变量(自动)具有本地范围,块范围的变量声明(如for循环体内的变量声明)默认为auto存储类型。
可用于区间for迭代:
std::vector<int> vec = { 1, 2, 3, 4 };
if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end())
*itr = 4;
//像python
for (auto it : vec)
std::cout << it << std::endl; // read only
for (auto &it : vec)
it += 1; // writeable
explicit
修饰构造函数,防止隐式转化 是针对单参数的构造函数而言,对构造函数很有用! 不能对有多个参数的构造函数使用
new malloc
都是用来申请内存分配,malloc是库函数,new是C++ 定义的关键字 malloc在堆上分配一块内存,new在自由存储区分配一块内存并给对象初始化 使用new内存分配失败时,会抛出异常,要用try catch来检测;malloc分配内存失败时返回NULL,用p==NULL来检测 使用new申请内存时无须指定大小,编译器会根据类型信息自行计算,而malloc则需要明确指定大小 new/delete会调用对象的构造/析构函数,而malloc则不会 在C++这种偏重OOP的语言,使用new/delete更合适
数据类型
基本数据类型
补充:
扩展精度浮点型:long double
求数字位数:int型可用log10(num) // num需为正数
进制:8进制数前面要加0,16进制前面加0x,8进制和16进制只能表达无符号的正整数;C++不提供二进制数的表达方法
注意:不同数据类型之间(如int和longlong)进行运算时非常耗时(可以都用longlong)
指针
指针在32位机器上是4字节,在64位机器上是8字节。因为指针就是地址,地址就是指针,而地址是内存单元的编号,所以,一个指针占几个字节,等于是一个地址的内存单元编号有多长。(1字节=8位)参考
char *p[10]:[ 的优先级高于*,意味着p是一个有十个元素的数组,元素的类型是 char*
char (*p)[10]:"("的优先级最高,意味着p是一个指针,指向一个有十个char元素的数组
类型推导
有 auto
和 decltype
register变量:寄存器变量,只能修饰局部变量,不能修饰全局变量和函数,register变量必须是一个单个的变量,变量长度小于等于寄存器长度(很小)
auto
类型推导和返回值占位 一个占位符,使得静态类型也能够实现类似于动态类型的类型推导功能,不能类型转换或者sizeof等操作。 返回值占位:auto add(T a,T b) ->decltype(a+b){return a+b;} auto a = add(1,0.3); 函数模板里不能作为参数类型,但可以作为函数返回值类型 类似引用需要初始化值。可以节省代码量和增强可读性 自动去const常量性、volatile易失性,指针和引用除外
for(std::vector<int>::iterator it = vec.cbegin(); itr != vec.cend(); ++it)
//使用auto可简写为:
for(auto it = vec.cbegin(); itr != vec.cend(); ++it)
//C++20起,可用于函数传参
int add(auto a, auto b)
{
return a+b;
}
auto a=1;auto b=2;
std::cout<<add(a,b);
//auto还不能推导数组类型!!!
decltype
auto
关键字只能对变量进行类型推导,所以有了decltype,用法类似 typeof
:
auto 不能用于函数传参,不能用于推导数组类型 decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的: 类型推导:auto x = 1, y = 2; 类型推导:decltype(x+y) z; 拖尾返回类型:auto add(T x, U y) -> decltype( x+y ) { return x+y; } 利用 auto 关键字将返回类型后置,返回值类型随参数变化 如果用decltype(x+y) add(T x, U y); 在编译器读到 decltype 时,x 和 y 尚未被定义,因此不能通过编译
auto x = 1;
auto y = 2;
decltype(x+y) z; //等价于int z;
decltype(10) w; //int w;
尾返回类型推导
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}
//C++11 利用 auto 关键字将返回类型后置
template <typename T>
auto add(T x, T y)
{
return x+y;
}
//C++14后直接这样即可
decltype(auto)
无需显式的指定 decltype
的参数表达式,本质是对括号auto的表达式替换,然后再decltype推断。
int all=1;
decltype(auto) da11 = a11; // int型,拷贝
//decltype(all) da11 = a11;
decltype(auto) da111 = (a11); // int&型,引用
//示例
vector <int> a = { 1,2,3 };
auto pos = a.begin();
auto& elem = *pos;
decltype(*pos) elem1 = *pos;
decltype(auto) elem2 = *pos;
cout << *pos << elem1 << elem2;
类型转换
数据类型转换
// int to string:
int num = 123;
std::string str = std::to_string(num);
// string to int:
指针转换
const_cast
经由指针或引用,改变对象的底层const
如果对象本身是指针或引用,对象的底层是const,则使用强制类型转换获得写权限是合法的行为;如果对象本身是常量,使用const_cast再写变量,则结果未定义
dynamic_cast
向上或向下转型都是安全的,会进行RTTI,要求基类必须存在虚函数,只有bp实际指向的是Derived对象,才能转换成功
若指针类型的转换失败了,返回NULL。若引用类型的转换失败了,返回bad_cast异常
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast
static_cast在向上转换时是安全的,在下行转换时,不会进行RTTI,即使bp实际指向的是Base对象,转换也能成功,能够访问派生类的非虚函数和类成员,因此是不安全的
boost的conversion库
头文件: "boost/cast.hpp"
boost的conversion库由四个转换函数组成,分别提供了更好的类型安全性(polymorphic_cast), 更高效的类型安全防护(polymorphic_downcast), 范围检查的数字转换(numeric_cast), 以及文字转换(lexical_cast)。这些类cast函数共享C++转型操作符的语义。
C++标准库提供的动态转换操作符dynamic_cast既可以向上转换又可以向下转换,而boost库提供的类型转换运算符把这两种情况拆分开了。polymorphic_cast提供向上转换的操作,polymorphic_downcast提供向下转换的操作。
与C++的转型操作符一样,这些函数具有一个重要的品质,类型安全性,这是它们与C风格转型的区别:它们明确无误地表达了程序员的意图[2]。我们所写的代码的重要性不仅在于它可以正确执行。更重要的是代码可否清晰地表达我们的意图。这个库使得我们可以更容易地扩展我们的C++词汇表
polymorphic_cast
内部使用了dynamic_cast,可以实现交叉转换和多继承的类型转换操作符(基类A有两个直接派生类B、C。那么,将 pB 转换为 pC)(两个基类A1、A2,派生类B从A1、A2中派生,将pB转换为pA1或是pA2)
在错误时抛出一个 std::bad_cast
异常,比dynamic_cast
更能清晰地表明代码的意图
如果转型失败不应被认为是错误,则应该使用dynamic_cast
polymorphic_downcast
内部使用 static_cast 实现类型转换,并不动态检查类型转换是否合法,需要程序员自己确保转换安全
static_cast转换指针时不做检查(事实上没有虚函数/没开RTTI的情况下本来也没法做检查),当然可以向下转换,但能转换不意味着“正常运行”啊……C++也不对指针越界做检查,你越界了但没触发exception也不奇怪
在堆上new一个对象时,申请的空间通常都是比这个对象要大的,毕竟要记录额外的信息。所以你的访问虽然是UB但可能不会触发错误
numeric_cast
整数类型间的转换经常会产生意外的结果。例如有符号类型给无符号类型赋值,不同大小的整数类型间进行赋值
numeric_cast
通过测试范围是否合理来确保转换的有效性,当范围超出或转换失败时它会抛出异常boost::bad_numeric_cast
想查看你的系统上的类型大小,可以使用 sizeof(T)
或 std::numeric_limits<T>::max()
和 std::numeric_limits<T>::min()
lexical_cast
用于字符串与其它类型之间的字面转换,是功能性和优雅性的结合,是杰出程序员的伟大杰作,不要在需要时实现小的转换函数,更不要在其它函数中直接插入相关逻辑,应该使用 lexical_cast
这样的泛型工具。它有助于使代码更清晰,并让程序员专注于解决手上的问题。
可用于字符串类型和数值类型的相互转换,或你的自定义类型支持的所有字面转换
使用dynamic_cast函数效率比较低,因为是运行时开销,我们最好使用static_cast,然而使用static_cast函数向下转型存在极大的危险,而且可能产生错误。所以,我们用dynamic_cast进行测试,安全后再采用static_cast进行向下转换;polymorphic_downcast函数使用dynamic_cast进行测试(仅在调试模式下有效),然后使用static_cast进行转换操作(两者兼顾,皆大欢喜!)
B* b = new B();
A* a = b;
A* c = boost::polymorphic_cast<A*>(b); // 向上转型
B* d = boost::polymorphic_downcast<B*>(a); // 向下转型
初始化
可以在if/switch中声明新变量,就像在for里int i;
vector<int> vec = { 1, 2, 3, 4 };
if (const vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) *itr = 4;
}
右值引用
左值,就是赋值符号左边的值,能代表一个地址。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象
右值,不能出现在赋值等号的左侧,是指表达式结束后就不再存在的临时对象,分为纯右值、将亡值
左值引用,一般的引用,别名
右值引用,和左值的区别是,能绑定到临时变量,但不能取地址,没有名字,是临时的
深拷贝
在拷贝的时候复制这个char指向的堆对象,浅拷贝:C++默认,不复制,直接用指针指向这个堆对象,这时候有两个指针指向同一个对象。当一个对象析构的时候,会把堆对象释放掉,而另一个对象不知情,可能出错。 但是深拷贝(复制构造)要自定义构造函数,还有空间浪费。所以有了移动构造函数std::move(),就是让这个临时对象原本的内存的空间转移给构造出来的对象,相当于把它移动过去了。 std::move()用来把一个左值变成右值引用,即让另一个对象来接管它的资源(让B的指针指向这个堆对象,A的指针指向null),避免不必要的拷贝操作
引用
1.将亡值,需要右值引用:T &&
2. move这个方法将左值参数无条件的转换为右值
std::string lv1 = "string,"; // lv1 是一个左值
// std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值
std::cout << rv1 << std::endl; // string,
const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
// lv2 += "Test"; // 非法, 常量引用无法被修改
std::cout << lv2 << std::endl; // string,string,
std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
std::cout << rv2 << std::endl; // string,string,string,Test
reference(rv2); // 输出左值
移动语义
传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。 右值引用的出现解决了这个问题
std::string str = "Hello world.";
vector <string> v;
v.push_back(str);//拷贝,开销大
v.push_back(move(str));//移动,完成后str内为空
//智能指针unique
完美转发
为了让我们在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右保持右),所以使用 std::forward
来进行参数转发(传递)
void pass(T&& v)
{
ff(v);//一直是左值
ff(std::move(v));//一直是右值
ff(std::forward<T>(v));//左对左,右对右
}
int v=1;
pass(v);
输入输出
getline():C++标准库函数,它有两种形式,一种是头文件< istream >中输入流成员函数;一种在头文件< string >中普通函数。遇到以下情况发生会导致生成的本字符串结束:(1)到文件结束,(2)遇到函数的定界符,(3)输入达到最大限度。
#include <iostream> cin.getline(str, size, letter='');
#include <string> getline(cin, str, letter='');
cin >> str:
容器
序列容器: vector向量 相当于一个数组 list双向链表 deque 双端队列
容器适配器: queue队列,底层类默认为deque stack栈,默认的底层实现为vector 封装了一些基本的容器,使其具备新的函数功能
序列 | vector 双端队列deque 双向链表list |
---|---|
关联 | set map |
无序 | unordered_map unordered_set unordered_multimap unordered_multiset |
容器适配器 | stack queue |
其他 | tuple string |
初始化列表
所有标准库的容器均可,自己写的类也可以(需要其它操作)。 vector <int> v = {1, 2, 3}; vector <set<int> > vs ={{1, 2}, {1, 2, 3}}; 可嵌套初始化
initializer_list
,允许构造函数或其他函数,像参数一样使用初始化列表
#include <initializer_list>
#include <vector>
class mlist2 {
public:
vector<int> a;
mlist2(initializer_list<int> list) {
1. for (auto it : list) a.push_back(it);
//this, or
2. for (initializer_list<int>::iterator it = list.begin(); it != list.end(); ++it) a.push_back(*it);
}
void mout() {
for (auto it : a) cout << it << ' ';
}
};
int main(void) {
mlist2 m = {1,2,3,4,5,6}; //列表
//初始化列表除了用在对象构造上,还能将其作为普通函数的形参
m.mout();
}
向量vector
可以简单的把vector理解为,其内部有一个void*指针,用于指向在堆上申请的空间。
algorithm头文件: 定义了排序sort,反转reserve,求最值max_element,min_element,等算法。
numeric:定义了求和accumulate(起始迭代器, 结束迭代器, 初始值, 自定义操作函数),等算法。
vector<T> v(容量, 初始值);
vector<vector<T>> v(row容量, vector<T>(col容量, 初始值));
v.push_back/pop_back();
v.front/back(); //返回第一个/最后一个元素的引用
v.begin/end/rbegin/rend();
v.cbegin/cend(); //常量迭代器,不能修改数组值
v.empty/clear();
v.size();//有效位数
v.capacity();//容量(最多放多少个
v.shrink_to_fit(); //压缩capacity到size
v.reserve(); //只修改capacity大小,不修改size大小,
v.resize(int num); //既修改capacity大小,也修改size大小。
v.insert(v.begin(), 10); //头部插入10
v2.insert(v2.begin()+2, row); //插入一行,第二行前插入一行row数组
v.erase(v.begin(),v.end()); //删除两指针之间的数据项,前闭后开
v.erase(v.begin()); //删除指针(迭代器)指向的数据项
v2.erase(v.begin()+1, v.end()+2); //删除一行,删除第一行
v.swap(v1); //两个数组v1和v交换
reverse(v.begin(), v.end());//翻转数组
sort(v.begin(), v.end());//数组顺序排序
sort(v.rbegin(), v.rend());//数组逆序排序
sort(v.begin(), v.end(), [&](vector<int>a , vector<int> b){
return a[0] < b[0] || ((a[0] == b[0]) && (a[1] < b[1]));
}); // 条件排序
T minNum = *min_element(v.begin(), v.end());
T maxNum = *max_element(v.begin(), v.end()); // 求最值
auto a = find(v.begin(), v.end(), 5);
if(a!=v.end()) int b = *a; //查找到
//遍历:
for(auto x : a)
cout << x;
for (auto y = a.begin(); y != a.end(); y++)
cout << *y;
for(int i = 0; i< a.size(); i++)
cout << a[i];
//2.用过resize后可能有错
for (auto y = a.front(); y <= a.back(); y++)
cout << y;
双端队列deque
双端队列容器,和vector很像;
-
也擅长在序列尾部添加或删除元素(时间复杂度为
O(1)
),而不擅长在序列中间添加或删除元素 -
也可以根据需要修改自身的容量和大小
begin() | 返回指向容器中第一个元素的迭代器。/end/rend/cend... |
---|---|
size() | 返回实际元素个数。/max_size() |
resize() | 改变实际元素的个数。 |
empty() | 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 |
shrink _to_fit() | 将内存减少到等于当前元素实际所使用的大小。 |
at() | 使用经过边界检查的索引访问元素。 |
front()/back() | 返回第一个元素的引用。 |
assign() | 用新元素替换原有内容。 |
push_back()/pop() | 在序列的尾部添加一个元素。 |
push_front()/pop() | 在序列的头部添加一个元素。 |
insert() | 在指定的位置插入一个或多个元素。 |
erase() | 移除一个元素或一段元素。 |
clear() | 移出所有的元素,容器大小变为 0。 |
swap() | 交换两个容器的所有元素。 |
emplace() | 在指定的位置直接生成一个元素。 |
emplace_front() | 在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程。 |
emplace_back() | 在容器尾部生成一个元素。和 push_back() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程。 |
双向链表list
双向链表;#include <list>
begin() | 返回指向容器中第一个元素的双向迭代器。/end/rend/cend... |
---|---|
empty() | 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 |
size()/max_size() | 当前容器实际包含的元素个数 /容器所能包含元素个数的最大值 |
front()/back() | 返回第一个元素的引用。 |
assign() | 用新元素替换容器中原有内容。 |
emplace_front() | 在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。 |
emplace_back() | 在容器尾部直接生成一个元素。该函数和 push_back() 的功能相同,但效率更高。 |
push_front() | 在容器尾部插入一个元素。/push_back/pop_front() |
emplace() | 在容器中的指定位置插入元素。该函数和 insert() 功能相同,但效率更高。 |
insert() | 在容器中的指定位置插入元素。 |
swap() | 交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的。 |
resize() | 调整容器的大小。 |
erase() | 删除容器中一个或某区域内的元素。 |
clear() | 删除容器存储的所有元素。 |
remove(val) | 删除容器中所有等于 val 的元素。 |
remove_if() | 删除容器中满足条件的元素。 |
unique() | 删除容器中相邻的重复元素,只保留一个。 |
splice() | 将一个 list 容器中的元素插入到另一个容器的指定位置。 |
merge() | 合并两个事先已排好序的 list 容器,并且合并之后的 list 容器依然是有序的。 |
sort() | 通过更改容器中元素的位置,将它们进行排序。 |
forward_list 单项链表,不提供size();
字符串string
——构造函数:
string s(str); //生成str的复制品
string s(str, index, n); //将str内“始于index且长度为n”的部分作为s的初值
string s(str, len); //将str前len个字符作为s的初值
string s(str.begin(), str.end()); //以str [begin, end)作为s的初值
string s(n, c); //生成s包含n个c字符
——增删:
= //赋新值
+ //串联字符串
swap() //交换两个字符串的内容
+=,s.append(str),s.push_back("123"); //在尾部添加字符
s.pop_back/ clear();
s.erase(s.begin(), s.end()); //前闭后开
substr(start, num); //剪切得到子字符串
str1.copy(str2,len);
str1.copy(str2,len,pos); //复制
——替换:
s.replace(pos, n, n1, char c); //将当前字符串从pos索引开始的n个字符,替换成n1个字符c
s.replace(s.begin(), s.begin()+5, str); //将迭代器[iter1,iter2)区间替换为字符串str
s.replace(s.find("@"), n, str); //将当前字符串从pos索引开始的n个字符,替换成str
——读取:
c_str(); //将内容以C_string返回
data(); //将内容以字符数组形式返回
——大小:
s.size/ max_size/ length/ empty();
capacity() //返回重新分配之前的字符容量
reserve() //保留一定量内存以容纳一定数量的字符
——查找:
s.find (str, pos); //从当前字符串的pos索引位置开始,查找str
s.find (char c, pos); //查找字符c
s.rfind (str, pos); //反向查找str
s.rfind (char c, pos); //反向查找字符c
// 返回找到的位置索引,-1表示查找不到字符
——比较:
s.compare (str); //将s与str比较
s.compare (pos, len, const char* str); //将s从Pos索引位置开始的len个字符与str比较
s.compare ("123"); //将s与"123"比较
// 大于str返回1,小于则-1,相等则0
——迭代器:
begin(), end(), rbegin(), rend() //类似STL的迭代器支持
// 迭代器失效:会引起底层空间改变的操作,比如:resize() reserve() insert() push_back()等,
// 有可能会出现空间扩容也就是原来的空间被释放掉,数据被拷贝到了新空间,但是迭代器依旧指向的是
// 旧空间,此时对迭代器进行操作,就会访问到已经释放的空间,所以会出现迭代器失效
关于sizeof(string):
string的实现在各库中可能有所不同,但是在同一库中相同一点是,sizeof(string)和字符串的长度无关,在一个系统中所有的sizeof(string)是一个固定值(和编译器相关),字符串存储在堆上,属于动态分配的空间。 sizeof(string)=4是最典型的实现之一,不过也有其它的实现。
栈stack
stack<int> s; s.size( ) :栈中元素个数 s.top( ) :返回栈顶的元素 s.pop( ) :从栈中取出并删除元素 s.push(e) :添加元素e s.empty( ) :栈为空时返回true
set vs map: 存储不重复的值 根据元素的值自动由小到大排列(有序性) 高效的插入删除操作 集合以 [value, value]的形式储存,字典以 [key, value] 的形式储存,set可以做数组去重,map没有格式限制,可以做数据存储 map是key-value型容器,其中key是关键字,起到索引作用,而value就是其对应的值,仅map支持下标访问
为何map和set的插入删除效率比用其他序列容器高 因为关联容器不需要做内存拷贝和内存移动,容器内元素以节点的方式来存储,节点结构类似于链表,指向父节点和子节点。在插入时只需要把节点的指针指向新的节点,不需要移动内存
先进后出
-
top():返回一个栈顶元素的引用,类型为 T&。如果栈为空,返回值未定义。
-
push(const T& obj):可以将对象副本压入栈顶。这是通过调用底层容器的 push_back() 函数完成的。
-
push(T&& obj):以移动对象的方式将对象压入栈顶。这是通过调用底层容器的有右值引用参数的 push_back() 函数完成的。
-
pop():弹出栈顶元素。
-
size():返回栈中元素的个数。
-
empty():在栈中没有元素的情况下返回 true。
-
emplace():用传入的参数调用构造函数,在栈顶生成对象。
-
swap(stack<T> & other_stack):将当前栈中的元素和参数中的元素交换。参数所包含元素的类型必须和当前栈的相同。对于 stack 对象有一个特例化的全局函数 swap() 可以使用。
队列queue
先进先出
元组tuple
不易遍历,如果想让一个函数返回多个不同类型的值的话可以使用tuple。
其他情况有待探索。
//初始化:
tuple<int, char> t (1, '2');
auto t = make_tuple(1, '2');
//修改内容
t = tuple<size_t, size_t>(1, '8');
//成员访问
cout<<get<0>(t);
cout<<get<int>(t);//只有一个int
//获取元素个数
size_t num = tuple_size<decltype(a)>::value;
//拼接
auto new_t = tuple_cat(t, t2, t3);
//交换
t.swap(t2);//仅限两组个数一致
//解包
tie(a, b, ignore) = t;
//tie参数个数与元组一致,否则用ignore占位
//比较
当两个tuple具有相同数目的成员,且对应成员类型可比较的时候,tuple比较才合法
t1{1,2};
t2{1,3};
t2>t1;//返回true
//make_tuple
#include <tuple>
std::tuple<int, double, std::string> f(){
return std::make_tuple(2, 3.4, "abc");
}
//main():
auto [a,b,c]=f();
cout<<a<<b<<c;
关联容器map
#include < map >
map<int, string> m;
map<string, int> m1;
map<key, value> m2(m); //创建m的副本m2 m.size( ) :map中元素个数 m.count(2):map中元素2的个数
插入: m.insert(pair<int, string>(1, "one"));
或下标插入:m[1] = "one"; m1["two"]=2;
查找: set(key, value)
删除: delete(key):通过键 key 移除对应的数据。
clear():全删除
迭代器删除:iter = m.find("123"); m.erase(iter);
int n= m.erase("123"); 删除键值为123的,成功返回1,否则0
auto iter = m.begin(); m.erase(iter):删除迭代器iterator指向的元素
或m.erase(first,second):删除first到second区间内的元素(左闭右开)
——遍历:
for(auto num: m)
cout<<num.first<<num.second<<endl;
或:数组方法m[i];
或迭代器:
for(i=m.rbegin(); i!=m.rend(); i++)
cout<<i->first;
查找:
get(key):通过键查找特定的数值并返回。
has(key):判断字典中是否存在键key。
map<int>::iterator ip = m.find(2); 查找并返回元素2的迭代器,如果没找到则返回end()
迭代器查找: map<int, string>::iterator iter; iter = m.find(1); //找到关键词1,在set中查找是二分查找
if(iter!=m.end())
cout<<iter->second;
else cout<<"false"; 访问: 使用迭代器访问,iter->first指向元素的键,iter->second指向键对应的值,用下标访问map中不存在的元素将导致在map容器中添加一个新的元素,这个元素的键即为该下标值,键所对应的值为空
无序unordered_map
#include <unordered_map>
// 使用同map
unordered_map<int, string> myMap={{ 5, "cd" },{ 6, "ab" }};
map和unordered_map
无序 map 容器,不会像 map 容器那样对存储的数据进行排序。
unordered_multimap 和 unordered_map 的区别是,multimap允许储存多个键相同的键值对。
map | unordered_map | |
内部实现机理 | map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。map中的元素是按照二叉搜索树(又名二叉排序树,其左子树所有节点的键值都小于根节点,右子树所有节点的键值都大于根节点)存储的,使用中序遍历可将键值按照从小到大遍历出来。 | unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),因此可用于海量数据处理)。 |
优点 | 有序性:map结构最大的优点 高效:红黑树,内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现 | 因为内部实现了哈希表,因此其查找速度非常的快 |
缺点 | 空间占用率高,因为map内部实现了红黑树,每一个节点都要额外保存父节点、孩子节点和红/黑性质 | 哈希表的建立比较耗费时间 |
适用性 | 对于那些有顺序要求的问题,用map会更高效 | 对于查找问题,unordered_map会更高效 |
哈希表:
当HashMap的长度是2的n次幂时,就可以用位运算代替取余运算,计算更加高效。
- 扩容:
- 先插后判断是否需要扩容(尾插法)
- 扩容条件:当前存储的数量大于等于阈值
- 扩容后对table的调整:table容量变为2倍,但是不需要像之前一样计算下标,只需要将hash值和旧数组长度相与即可确定位置。依靠 (hash & oldCap) == 0 判断 Node 中的每个结点归属于 low 还是 high。
- 哈希冲突:
- 链地址法、哈希函数、引入红黑树
红黑树:
- 红黑树:一种自平衡的二叉查找树,高效的查找树,也被称为平衡二叉B树。
- 平衡二叉树AVL:查找、插入和删除在平均和最坏情况下都是O(logn)。但如果在AVL树中插入或删除节点后,高度差大于1了,AVL树的平衡状态就被破坏,它就不再是一棵二叉树,为了让它重新维持在一个平衡状态,需要进行旋转处理,耗费成本。
- 这时红黑树来了。它在每个节点增加了一个存储位记录节点的颜色,RED OR BLACK,通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍)
- 1节点是红色或黑色,2根是黑色,3叶子节点(null节点)是黑色,4红色节点的子节点和父节点都是黑色,5从任一节点到叶子节点的所有路径都包含相同数目的黑色节点
- 插入的时候将节点设置为红色,可以保证满足性质 1、2、3、5 ,只有性质4不一定满足,需要进行相关调整。
关联容器set
元素都会在插入时自动被排序,且set不允许容器中有重复的元素
// 初始化:
set<int> s;
// 插入:
s.insert(val); // 插入val
// set<int> b = {3,4,5}; auto first = b.begin(); auto second = b.end();
s.insert(first, second); // 将first到second区间内元素插入到s
// 删除:
s.delete(key); // 关键字删除
s.clear(); // 全删除
s.erase(iterator); // 删除迭代器iterator指向的元素
s.erase(first,second); // 删除first到second区间内的元素(左闭右开)
s.erase(2); // 删除键值为2的元素
// 修改:
s1.swap(s2); // 交换两个集合变量
// 查找:
s.begin/ end/ rbegin/ rend();
s.has(val); // 查找,在set中查找是二分查找
s.count(val); // set中元素val的个数,很少用,因为一个键值在set只可能出现0或1次
set<int>::iterator ip = a.find(val); // 判断字典中是否存在val,查找并返回val的迭代器,没找到时返回end()
// 元素个数:
s.size/ max_size/ empty();
equal_range(); // 返回集合中与给定值相等的上下限的两个迭代器
get_allocator(); // 返回集合的分配器
upper_bound(); // 返回大于某个值元素的迭代器
lower_bound(); // 返回指向大于(或等于)某值的第一个元素的迭代器
key_comp(); // 返回一个用于元素键值比较的函数
value_comp(); // 返回一个用于比较元素间的值的函数
map/set
map | 定义在 <map> 头文件中,其各个元素的键必须是唯一的(不能重复),该容器会根据键值,默认进行升序排序(调用 std::less<T>)。 |
---|---|
set | 定义在 <set> 头文件中,各个元素键和值完全相同,且各个元素的值不能重复,该容器会根据键值自动升序排序(调用 std::less<T>)。 |
array
array <T,num> a = {1,2,3};
a.data(); //首指针
a1.swap(a2);
a.empty/size/max_size();
a.begin/end/front/back/cbegin/cend/rbegin/rend();
枚举enum
枚举常量代表该枚举类型的变量可能取的值,编译系统为每个枚举常量指定一个整数值,默认就是所列举元素的序号,序号从0开始 也可以在定义枚举类型时为部分或全部枚举常量指定值,在指定值之前的常量按默认方式取值,指定值之后的常量值依次加1 各枚举常量的值可以重复 枚举常量只能以标识符形式表示,而不能是整型、字符型等文字常量
enum week {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
enum week {Sun=7, Mon=1, Tue, Wed, Thu, Fri, Sat};
// TODO 下面的再确认一下对错
//1.弱类型: C++11前的枚举类型是默认为int类型,不是强类型的,所以毫无关系的两个枚举类型可以进行比较
if (EnumA::A == EnumB::C) {}
//且由于枚举类型的元素是完全暴露出来的,其作用域内不可以有同名元素
enum C{ E = 1, F = 2};
enum D{ E = 1, G = 2};//重定义,无法通过编译,EnumC::E与EnumD::E被认定为同一变量
//2.强类型: 将无法隐形转换为int类型
enum class A{one, two, three};
enum class B{one, two, three};
//用到的时候(可以避免两个枚举类的命名冲突):
A num = A::one;
//3.强类型支持指定枚举类型的数据类型,如:
enum A : int {};
enum class B : long {};
//4.由于可以指定enum的数据类型,所以enum支持前置声明啦~但必须指定类型
enum class A : char;
//中间内容
enum class A : char {one = '1', two = '2'};
可变体variant
C++17 提供了 std::variant
// 声明一个可变体的对象
// 在variant关键字的尖括号内,依次指定可变体的的数据类型
// 在可变体的内部,这些数据类型存在顺序关系
std::variant<int, double, std::string> tmp;
// 检测可变体内部可切换的数据类型的个数
auto num = std::variant_size_v<decltype(tmp)>;
// 访问可变体中的当前处于活动状态的数据类型的实例
std::visit(PrintVisitor {}, tmp);
// 返回当前可变体内部对应的数据类型的索引
std::cout << tmp.index() << std::endl; // 输出0 默认int
tmp = 100.00;
std::cout << tmp.index() << std::endl; // 输出1 double
// 判断可变体当前持有的数据类型
std::hold_alternative<>;
// std::get_if 和 std::get,两个方法的参数都可以是index(下标)或者T(类型)
// 其中get_if提供了访问前的类型安全判断,保证在访问可变体时不会抛出bad_variant_access异常
tmp = "hello super world"; // string
if(const auto intPtr (std::get_if<int>(&tmp)); intPtr)
std::cout << "int! " << *intPtr << '\n'; //intPtr不为真,所以不会执行
// 异常检测:
tmp = "hello super world"; // string
try
{
auto f = std::get<double>(tmp);
std::cout << "double! " << f << '\n';
}
catch(std::bad_variant_access&)
{
std::cout << "可变体内部当前持有的数据类型和get<>的传入参数类型不一致" << '\n';
}
运算符
简单运算符
- 算术运算符:+ - * / % ++ --
- 关系运算符:== != > < >= <=
- 逻辑运算符:&& || !
- 位运算符(二进制):& | ^ ~(取反) <<(左移) >>(右移)
- 赋值运算符:= 算术运算符= 位运算符=
- 杂项运算符:
- ?: 条件运算符。auto a = (条件1) ? 内容1 : 内容2
- sizeof运算符。一个编译时运算符,可用于获取类、结构、共用体和其他用户自定义数据类型的字节大小。也是一个关键字。sizeof(string) sizeof(int)
- .和->成员运算符。用于引用类、结构和共用体的成员。
- Cast强制转换运算符。把一种数据类型转换为另一种数据类型
- &指针运算符。返回变量地址。
- *指针运算符。指向一个变量。
重载运算符
- 重载后的运算符的操作对象必须至少有一个是用户定义的类型
- 不能修改运算符原先的优先级、语法规则
- 不能创建一个新的运算符
- 不能重载的运算符:成员运算符,作用域运算符,条件运算符,sizeof运算符,typeid(一个RTTI运算符),const_cast、dynamic_cast、reinterpret_cast、static_cast(强制类型转换运算符)
指针
指针和引用
指针int *p = &m; 引用int &n = m; 指针有自己的一块空间,而引用只是一个别名; 使用sizeof看一个指针的大小是4字节,而引用是被引用对象的大小; 指针可以被初始化为NULL,而引用必须被一个已有的对象初始化; 指针在使用中可以指向其它对象,但是引用不能改变引用对象,可以把引用看做指针常量; 指针可以有多级指针**p,而引用只有一级;
函数的参数和返回值的传递方式: 值传递、指针传递和引用传递; 指针传递:void Func2(int *x) Func2(&n); 引用传递:void Func3(int &x) Func3(n);
常量指针:const int *p:const修饰常量; 指针常量:int * const p:const修饰指针; 常引用:引用时保护数据不被改变。声明方式:const int &a1 = a;
//const常量的指针//
const int a = 10;
int b=100;
int *p= (int *)&a;
int *u= &b;
*u=20;
cout<<a<<b<<endl;
野指针和指针悬挂
野指针:不是空指针,是未初始化的指针
空指针:int *p = NULL,是经过初始化或赋值NULL的指针
悬空指针:指针指向的对象被释放,指针指向已经被回收的内存地址(⽆效内存区域)
规避野指针:指针初始化、小心指针越界(数组)、指向空间释放及时置NULL、避免返回局部变量的地址、使用之前检查有效性
指针越界
内存中同时存在多个进程,OS为每个进程都分配了内存空间 进程对自己所拥有的内存具有访问(读写)权限,当指针访问不属于当前进程所拥有的内存时,就会程序崩溃
堆区外的空间都是在编译时就确定的,无法改变,只有堆区是可以动态扩充的,且有个初始大小 char *c = new char; 是在当前进程的堆区中开辟一小块内存(1Byte),c拥有对这块(1Byte)内存操作的权利。
但实际上,指针c也拥有访问当前进程整个堆区的权利,所以指针越界未必会报错
智能指针与内存管理
智能指针: std::shared_ptr
/ std::unique_ptr
/ std::weak_ptr
,头文件 <memory>
/*1.不支持拷贝构造和赋值运算函数
unique_ptr<int> ap(new int); 对
unique_ptr one(ap); 错
unique_ptr two = one; 错*/
//2.可以移动构造和移动赋值操作
unique_ptr<int> Getobj();
{
unique_ptr<int> ptr(new int);
return ptr;
}
unique<int>ptr = Getobj;
unique_ptr<int>ptr1(new int());
unique_ptr<int>ptr2(std::move(ptr1));
设有结构体:struct somedata{int a, b;};
创建指针:unique_ptr<somedata> data(new somedata);
创建指针:auto data = make_unique<somedata>(); //如果构造函数中抛出了异常,不会返回野指针
使用指针:data->a = 1;
普通指针的问题:一是忘记释放内存,造成内存泄漏;二是尚有指针引用内存的情况下释放内存,产生引用非法内存的指针;三是指针指向允许内存之外的地址
以前使用 new
和 delete
,而智能指针使用引用计数的想法,自动释放内存。 一定程度上消除了 new
/delete
的滥用,是一种更加成熟的编程范式
shared_ptr
std::shared_ptr
记录有多少个 shared_ptr
共同指向一个对象,从而消除显式的调用 delete
,当引用计数变为零的时候将对象自动删除
auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // 引用计数+1
auto pointer3 = pointer; // 引用计数+1
int *p = pointer.get(); // get不增加引用计数
// 可以通过 get() 方法来获取原始指针,通过 reset() 来减少一个引用计数,并通过use_count()来查看一个对象的引用计数
(*pointer)++;
std::cout << *pointer << std::endl; // 11
std::cout << "对象引用次数 = " << pointer.use_count() << std::endl;
// 3
pointer2.reset(); //减少引用计数
unique_ptr
禁止其他智能指针共享同一个对象,从而保证代码的安全。
auto pointer = std::make_unique<int>(10);
// unique_ptr<int> pointer = std::make_unique<int>(10);
weak_ptr
std::shared_ptr
依然存在着资源无法释放的问题,如上图,这时可以用弱引用。弱引用不会引起引用计数增加,没有 *
运算符和 ->
运算符,所以不能够对资源进行操作;
1.可以用于检查 std::shared_ptr
是否存在,其 expired()
方法能在资源未被释放时,会返回 false
,否则返回 true
;
2.可以用于获取指向原始对象的 std::shared_ptr
指针,其 lock()
方法在原始对象未被释放时,返回一个指向原始对象的 std::shared_ptr
指针,进而访问原始对象的资源,否则返回nullptr
。
this 指针
隐含于每一个非静态成员函数中的特殊指针,指向调用该成员函数的对象
当对一个对象调用成员函数时,编译程序将对象的地址赋给 this 指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this 指针 this 指针被声明为指针常量,因此不能修改 this 指针;在 const 成员函数中,this 指针的为常量指针常量,就是也不能修改指向的对象 this 并不是一个常规变量,所以不能取得 this 的地址(不能 &this)
空指针
C++11 引入了 nullptr关键字,专门用来区分空指针、0(NULL),而nullptr的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
NULL是一个无类型的东西,而且是一个宏。C++是一种强类型语言,编译时的检查可以帮我们找到很多类型错误,用NULL可能有歧义。
//nullptr可以区分空指针、0
//is_same判断是否相同
if (std::is_same<decltype(NULL), decltype(0)>::value)
std::cout << "NULL == 0" << std::endl;
if (std::is_same<decltype(NULL), decltype((void*)0)>::value)
std::cout << "NULL == (void *)0" << std::endl;
if (std::is_same<decltype(NULL), std::nullptr_t>::value)
std::cout << "NULL == nullptr" << std::endl;
//decltype作为操作符,用于查询表达式的数据类型。
函数
工具函数
// 1.求指数,幂
pow(a,b); //a的b次方
引用传递和指针传递
指针:变量,独立,可变,可空,替身,无类型检查; 引用:别名,依赖,不变,非空,本体,有类型检查;
指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,函数的形参作为它的局部变量处理,会在栈中开辟内存空间来存放,从而形成了实参的一个副本,当然我们可以使用解引用符(*)来访问和改变指针对应的内存中的变量内容 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,来存放由主调函数放进来的实参的地址。被调函数对形参的操作就是根据别名找到的本体 从编译的角度讲,程序在编译时会有个符号表,表里记录程序里的变量名及变量对应的地址,这个表生成之后就不能更改,也就是变量不可以改变地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用的地址值为引用对象的地址值
伪函数(仿)
就是使用起来像函数一样的东西。如果对某个class进行 operator 重载,它就是一个仿函数类,该函数就是一个仿函数
伪函数,就是一个类的使用看上去像一个函数,其实现就是 class 中实现一个 operator(),这个类就有了类似函数的行为,就是一个伪函数类了
//仿函数类
template<class T>
struct Greater
{
//仿函数
bool operator()(const T& a, const T& b)
{
return a > b;
}
};
//使用:
Greater<int> g;
bool ret;
ret = g.operator()(13, 14);
ret = g(13, 14); //简写形式-使用方法和函数类似
lambda表达式
基础
面向对象思想强调“必须通过对象的形式来做事情”,函数式思想则尽量忽略面向对象的复杂语法:“强调做什么,而不是以什么形式去做”,Lambda表达式就是函数式思想的体现
一个lambda表达式表示一个可调用的代码单元,我们可以理解为一个未命名的内联函数。它与普通函数不同的是,lambda必须使用尾置返回来指定返回类型;
可用于回调函数,可用作函数参数,可以捕获变量,接受输入参数
// 1语法形式:
auto name = [捕获列表](参数列表) opt -> 返回类型 {
// 函数体
// opt可以填mutable, exception, attribute(选填)
// 捕获列表可以理解为,参数的一种类型,Lambda 表达式内部函数体在默认不能使用函数体外部的变量,这时捕获列表可以传递外部数据
}
//例子1
auto f = [](int a) -> int {return a + 1;};
auto f = [](int a) {return a + 1;}; //自动推导类型
auto f = []{return 1;};// Lambda是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象(闭包)的简便方法
cout << f(3) << endl;//输出
//例子2
auto f = [](int a) -> int {return a + 1;};
cout << f(3) << endl; //输出4
int b = 6;
auto g = [b](int a) -> int {return a + 1;};
cout << g(3) <<endl; //输出9
//例子3
bool compare(int& a,int& b)
{
return a>b;
}
sort(a, a+n, compare);//降序排序
//可简化为:
sort(a, a+n, [](int a,int b){return a>b;});//代码简洁了
//例子4
[] (int x, int y) { return x + y; }(1, 2) // 隐式返回类型,系统自动推断
[] (int& x) { ++x; } // 没有 return 语句 -> Lambda 返回 'void'
[] () { ++global_x; } // 没有参数,仅访问某全局变量
//可以显示指定返回类型:
[] (int x, int y) -> int { int z = x + y; return z; }
//例子5,一个最简单的lambda表达式:
[] {};//可以通过编译
//例子6
int main() // C4297 expected
{
[]() noexcept { throw 5; }();
}
auto transformByVec = [&](std::vector<OcDb3dSolidPtr>& solids, const OdGeVector3d& vec)
{
for (int i = 0; i < solids.size(); i++)
{
solids[i]->transformBy(vec);
}
};
捕获:
//值捕获:按值捕获的变量不能修改.如果是按值捕获并且又想修改外部变量,可以显示指明lambda表达式为mutable:
[value]
//引用捕获:可修改
[&value]
//隐式捕获:
[] 不捕获任何变量
[&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)
[=] 捕获外部作用域中所有变量,并作为副本在函数体重使用(按值捕获)
[this] 捕获当前类中的this指针,让表达式拥有和当前类成员函数同样的访问权限,可以使用其成员变量和成员函数。使用了&或者=时默认添加此选项。
[=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获foo变量
[a] 按值捕获a变量,同时不捕获其他变量
//表达式(右值)捕获:
auto lizi = std::make_unique<int>(1);
auto add = [v1 = 1, v2 = std::move(lizi)](int x, int y) -> int {
return x + y + v1 + (*v2);
};
std::cout << add(3, 4) << std::endl;
泛型
auto
关键字不能用在参数表里,因为会与模板的功能冲突;但Lambda 表达式并不是普通函数,也不能模板化。
因此从C++14开始,Lambda 函数的形参可以使用 auto
关键字来产生意义上的泛型
auto add = [](auto a, auto b) {return a + b; };
工具函数
srand
srand:随机数发生器的初始化函数,它初始化随机种子,会提供一个种子,对应一个随机数,如果使用相同的种子后面的rand()函数会出现一样的随机数
如为了防止随机数每次重复,常常使用系统时间来初始化,即使用 time函数来获得系统时间,它的返回值为从 00:00:00 GMT, January 1, 1970 到现在所持续的秒数,然后将time_t型数据转化为(unsigned)型再传给srand函数,即: srand((unsigned) time(&t)); 或 srand((unsigned) time(NULL));
如果想在一个程序中生成随机数序列,只需在主程序开始处调用srand,后面直接用rand就可以了
函数对象包装器
function
int foo(int a) {
return a;
}
//包装了一个返回值为 int, 参数为 int 的函数
std::function<int(int)> func = foo;
std::cout << func(10) << std::endl;//输出10
bind+placeholders
用来绑定函数调用的参数, 可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用
int max0(int a, int b, int c) {
return a;
}
int main()
// 使用 placeholders::_1 来对第一个参数进行占位,然后2,3绑定到原函数上
auto max1 = bind(max0, placeholders::_1, 2, 3);
//只需第一个参数
cout<<max1(4);
}
类
虚函数
仅类的成员函数可以是虚函数,非成员函数,内联函数,构造函数,静态函数都不可以
虚函数表
每个包含了虚函数的类都包含一个虚表。虚表是一个指针数组,其元素是虚函数的指针,虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表里。
虚表是属于类的,而不是属于某个具体的对象,当一个类继承另一个类时,也会继承它的函数调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,也拥有自己的虚表。含有虚函数的类的大小要+(一个指针的大小),因为它有虚表的地址。
- 先将基类虚函数表拷贝一份到子类的表里
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增加的虚函数按其声明次序增加到派生类虚表的最后
- 多重继承时每个父类都有自己的虚表,子类的成员函数被放到了第一个父类的表中
- eg. 用父类指针调用虚函数,可能在运行时绑定到不同的子类中,产生不同的行为
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。虚函数表在编译时确定,而对象的虚函数指针在运行阶段才确定。
派生类菱形继承的话,会有两个虚指针,此时我们可以使用虚继承,删掉重复指针
类的大小:
空类的大小不是0:为了确保两个不同对象的地址不同。
类的实例化是在内存中分配一块地址,每个实例在内存中都有独一无二的二地址。同样,空类也会实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化后就有独一无二的地址了。所以,空类的sizeof为1,而不是0,多重继承的空类的sizeof也是1。
封装 继承 多态
封装
封装,继承是为了代码重用。
隐藏实现细节,使得代码模块化。
继承
不用重新编写原来的类,也可以扩展已存在的代码模块(类),派生类继承基类虚函数的接口
继承与组合:is a 和has a 的关系,组合是在B类里定义一个A类的对象,低耦合,继承是高耦合 继承与友元:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,友元函数不是类成员,但可以访问类的所有成员 继承与静态成员:基类定义了static静态成员,则整个继承体系里面只有一个这样的成员 赋值:派生类对象可以赋值给基类对象,基类对象不能赋值给派生类对象,但基类的指针可以通过强制类型转换赋值给派生类的指针
1先调用基类的构造函数 2如果派生类构造函数列表中包含对基类子对象成员的初始化(如St2类里定义了一个St1 s1;),每个基类子对象初始化时也要调用一次基类构造函数; 3最后才是派生类自己的构造函数来初始化自身新增的成员,执行顺序就是派生类构造函数列表顺序 执行顺序就是派生类构造函数列表顺序
多重继承
派生类都只有一个基类,称为单继承,派生类可以有两个或多个基类,称为多继承;
class D: public A, private B, protected C{
//类D新增加的成员
D(形参列表): A(实参列表), B(实参列表), C(实参列表)
{
//无论此处ABC顺序如何,基类构造函数的调用顺序只和声明派生类时基类出现的顺序相同,即A、B、C
//其他操作
}
}
接口(抽象类)
描述了类的行为和功能,而不需要完成类的特定实现,不能被用于实例化对象;如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类
抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念
class A{
public:
// 纯虚函数:通过在声明中使用 "= 0" 来指定
virtual double GetArea() = 0;
}
class B : public A{
public:
// 纯虚函数实现
double GetArea(){
return (width*height);
}
}
class C : public A{
public:
// 两个派生类通过不同的计算面积的算法来实现这个相同的函数
double GetArea(){
return (3.14*radius*radius);
}
}
组合
继承是为了实现代码的复用,如果A类和B类毫无关系,我们不应该为了让B类多一个功能而去让B继承A;
如果,eye、mouse、nose这些类都是Head类的一部分,是(is -a-part-of)而不是(is -a-kind-of)的关系,则可以通过组合来实现:
class Head
{
public:
void Look(void){
m_eye.Look();
}
void Smell(void){
m_nose.Smell();
}
void Eat(void){
m_mouth.Eat();
}
private:
Eye m_eye;
Nose m_nose;
Mouth m_mouth;
};
int main()
{
Head man;
man.Look();
}
多态
多态是同一个行为具有多个不同表现形式或形态的能力。(接口重用:一个接口,多种方法,同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。)
静态多态:函数重载。允许在同一作用域中声明几个类似的同名函数,这些同名函数的形参列表(参数个数,类型,顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
动态多态:函数重写。允许用一个或多个派生类对象的属性配置父类对象。在多态性的支持下,父类对象的某个接口会随着派生类对象的不同而执行不同的操作。
联编:也称绑定。是指在一个源程序经过编译链接成为可执行文件的过程中,将可执行代码“缝合”在一起的步骤。其中在程序运行前就完成的称为静态联编(前期联编);在程序运行时完成的称为动态联编(后期联编)。
静态联编和动态联编:静态联编支持的多态性称为编译时多态(静态多态),通过函数重载或函数模板实现;动态联编支持的多态性称为运行时多态(动态多态),通过函数重写/虚函数表实现。
抽象基类和纯虚函数:通过令虚函数为0可以表示纯虚函数,纯虚函数只定义了函数接口,包含纯虚函数的类称为抽象类。抽象类定义了一个类可能发出的动作的原型,但既没有实现,也没有任何状态信息。引入抽象类的原因在于,很多情况下基类本身实例化不合情理。其派生类必须提供接口的具体实现,否则亦无法被实例化。
重载(overload)和重写(override)是实现多态的两种主要方式:
-
重载:在同一作用域中,参数不同但语义相近的几个同名函数,通常用函数模板来实现;
-
重写:子类覆盖基类 virtual 函数,函数名相同、参数相同,返回值相同,访问修饰符可以不同;
-
重定义:基类virtual随意,函数名相同、参数不同or参数相同且基类无virtual时,隐藏基类函数,除非在调用时强制转换为父类类型
//重载//
void decode(int *a, int *b){
int temp = *a + 1;
*a = *b;
*b = temp;
}
void decode(float *a, float *b){
float temp = *a + 10.0;
*a = *b;
*b = temp;
}
//重写//
public:
Entity(){};
virtual void Func(int a)
{
std::cout << "Entity" << std::endl;
}
};
class Person : public Entity {
public:
Person() {};
void Func(int a)
{
std::cout << "Person" << std::endl;
}
};
int main(){
Entity* e = new Person();
e->Func(0); // 输出 Person
}
动态绑定和静态绑定
对象的静态类型:声明时采用的类型,在编译期确定
对象的动态类型:目前所指对象的类型,在运行期决定
静态绑定:绑定对象的静态类型
动态绑定:绑定对象的动态类型。动态类型可以更改,静态类型不能更改
//eg: 父类A,子类B,子类C
B* b=new B(); A* a=b;
//则a的静态类型是它声明的类型A*,动态类型是B*
C* c=new C(); a=c;
//则a的动态类型现在是C*
(非虚函数静态绑定,虚函数动态绑定;比如指针a,有A的非虚函数和C的虚函数) 指针和引用的动态类型和静态类型可能会不一致,但是对象的动态类型和静态类型是一致的 意义:用基类指针指向派生类,如果有基类指针的数组,就可以把基类和派生类对象放在一组管理,当不同的派生类有类似的行为,就可以直接调用不同派生类的同名函数
RTTI
C++的运行时多态性由虚函数实现,对于多态性的对象,无法在程序编译阶段确定对象的类型,当类中含有虚函数时,其基类的指针就可以指向任何派生类的对象,类型的具体要在运行时利用运行时类型标识来确定
RTTI提供了两个有用的操作符:typeid和dynamic_cast;
-
typeid: 头函数<typeinfo>,让用户知道当前的变量是什么类型的,比如使用typeid(a).name()返回变量a是什么类型的,返回类型为typeinfo类对象(包含name()、 raw_name()、 before()、 ==、 != 等函数和操作符)的引用
-
dynamic_cast: 强制转换运算符,用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用,只能用于含有虚函数的类,表达式为dynamic_cast<类型>(表达式)。如:A *a1; B *b1, b2; a1=&b2; b1=dynamic<B *>(a1); 最后一条语句表示把指向派生类B的基类指针a1转换为派生类B的指针,然后将这个指针赋给派生类B的指针b1
其它的强制转换运算符:static_cast,reinterpret_cast,const_cast
友元
通过友元,一个普通函数或者另一个类中的成员函数可以访问本类中的私有成员和保护成员。友元破坏了类的封装性和数据的隐藏性,导致程序可维护性变差
友元函数: 有元函数是可以访问类的私有成员的类外普通函数,需要在类的定义中加以声明 friend 类型 函数名(形式参数); 一个函数可以是多个类的友元函数,只需要分别声明
友元类: 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的数据信息 friend class 类名;
注意:1友元关系不能被继承;2友元关系单向。若类B是类A的友元,类A不一定是类B的友元 3不具有传递性。若B是A的友元,C是B的友元,C不一定是A的友元
struct和class
struct 可以看成数据结构的实现体,class 可以看成对象的实现体。
区别:默认的访问控制。struct 是 public 的,class 是 private 的。C++中,struct可以被继承,可以包含成员函数,也可以实现多态
如果没有构造函数,struct可以使用{ }初始化;若class的所有数据成员及函数为public时,可以使用{ }初始化;
union
一种节省空间的特殊的类,可以有构造、析构函数,默认 public,不能继承或被继承,不能含有虚函数,不能含有引用类型的成员
匿名 union 不能包含 protected 成员或 private 成员,在定义所在作用域可直接访问 union 成员,全局匿名联合必须是静态(static)的
::范围解析运算符
全局作用域符(::name):用于指定类型的作用域范围,为全局命名空间 类作用域符(class::name):为具体某个类的 命名空间作用域符(std::name):为具体某个命名空间的
类初始化
委托构造
在同一个类中,用一个构造函数调用另一个构造函数。可以简化代码:
允许在一个构造函数中调用另一个构造函数 test(int n){} test():test(0){}在调用后一个构造函数时,也自动调用前一个构造函数做一些初始化
class stu {
public:
int a, b;
stu() { b = 0; }
stu(int sa):stu() { a = sa; }
};
stu s(1);
继承构造
如果派生类想要使用基类的构造函数,需要在构造函数中显式声明。如果基类拥有众多不同版本的构造函数,在派生类中得写很多对应的构造函数。 原本要 B(int i):A(i){} B(double d,int i):A(d,i){}两遍 现在只需在B中 using A::A;一遍
//1.子类为完成基类初始化,在 C++11 之前,需要在初始化列表调用基类的构造函数,从而完成构造函数的传递
class A{
A(int m):a1(m),a2('0'){};
A(char n):a1(0),a2(n){};
int a1; char a2;
}
class B : public A{
B(int m) :A(m) {}
B(char n) :A(n) {}
}
//2.为了提高效率,C++11有了继承构造函数,使用using 来声明继承基类的构造函数
class B : A{
using A::A; // 使用继承构造函数
//继承构造函数与类的一些默认函数(默认构造、析构、拷贝构造函数等)一样,是隐式声明,更加节省目标代码空间
B(int v, int k):A(v), B1(k){}
//如果新增派生类构造函数,要使用构造函数初始化列表
int B1;
};
//3.多继承的情况下,继承构造函数会出现“冲突”的情况,如:
A(int i){}
B(int i){}
C : public A, public B{
using A::A;
using B::B; //编译出错,重复定义C(int)
//可以显示定义继承构造函数 C(int) 避免冲突
C(int i):A(i),B(i){}
}
注意:
1.继承构造函数无法初始化派生类数据成员;2.构造函数拥有默认值会产生多个构造函数版本,且继承构造函数无法继承基类构造函数的默认参数,此处需注意;3.多继承的情况下,继承构造函数会出现“冲突”的情况
显式虚函数重载
以前当基类的虚函数被删除后,子类拥有旧的函数就不再重载该虚拟函数并成为了一个普通的类方法。
C++11 引入 override
和 final
来防止该现象。
//override显式的告知编译器进行重载
父:virtual void a(int);
子:
virtual void a(int) override; // 合法
virtual void a(float) override; // 父类没有此虚函数,则非法
//编译器识别final类,final函数
struct Base {
virtual void a() final;
};
struct SubClass1 final: Base {
}; // 合法
struct SubClass2 : SubClass1 {
}; // 非法, SubClass1类已 final
struct SubClass3: Base {
void a(); // 非法, final函数a
};
显式禁用默认函数
stu() = default;//显式声明使用编译器生成的构造
stu& operator=(const stu&) = delete;//显式声明拒绝编译器生成的构造
stu() { b = 2; }
stu(int sa):stu() { a = sa; }
强类型枚举
C++11 引入了枚举类,并使用 enum class
的语法进行声明
enum class en : unsigned int {
value1 = 0,
value2 = 4,
value3 = 8,
value4 = 12
};
//可以通过重载 << 这个算符来进行输出
template<typename T>
std::ostream& operator<<(
typename std::enable_if<std::is_enum<T>::value,
std::ostream>::type& stream, const T& e)
{
return stream << static_cast<typename std::underlying_type<T>::type>(e);
}
cout << value3;
构造函数和析构函数
构造函数:构造函数不能声明为虚函数。一个对象的内存空间中存放着它的虚函数表指针,调用虚函数要通过这个指针,而在构造一个对象时,由于对象还未创建成功,编译器不知道对象的实际类型,是基类、还是派生类、还是其他,也没有虚函数表,不知道要调用哪个虚函数。
析构函数:如果析构一个指向派生类的基类指针,最好将基类的析构函数声明为虚函数,否则编译器将不会触发动态绑定,删除基类指针只会调用基类的析构函数,不调用派生类析构函数,会造成对派生类对象的析构不完全,造成内存泄露。
构造函数顺序:其他类的static成员>自己的父类>自己的其他成员>自己。析构函数刚好相反。
析构函数的执行时机:析构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。
在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。
静态成员
静态成员变量:在类里声明,在类外定义(初始化)。因为静态成员变量属于整个类,所以没有this指针,非静态成员函数可以直接使用静态成员变量。
静态成员不能在类内初始化的原因:因为静态成员static 属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的,常量成员const也不能在类内初始化。能在类中初始化的成员只有一种,那就是静态常量成员static const。
模板
模板/类/函数
模板类
是类模板实例化的产物
类模板
一种通用的类,编译器从类模板可以自动生成多个类,使类中的某些参数、或者成员函数的返回值,能够取任意类型(包括系统预定义的和用户自定义的)
template <class T> // template 关键字用于声明类模板
class Text
{
public:
// 构造函数、析构函数、其它函数
Text(T a);
private:
T* data;
int num;
};
// 类模板实例化对象:
T <int> t1(3, 5); // 类模板实例化对象时,不能自动类型推导
// 成员函数实例化(有参构造):
template <class T>
Text<T>::Text(T a)
{ 具体内容 }
// 成员函数实例化(无参构造):
template <class T>
Text<T>::Text()
{ 具体内容 }
模板函数
模板函数可以为函数模板传参,根据实际传入的参数类型生成的一个重载函数即为模板函数,它是函数模板的一次实例化。
函数模板
函数模板,是一个通用函数,它的函数类型和形参类型用一个虚拟的类型来代表,不具体指定。
它起源于C++的函数重载。以前通过函数重载我们能够实现一个函数名多用,将实现相同或者相似功能的函数用同一个函数名来定义,但是在程序中仍然要根据类型分别定义每一个函数。为了进一步简化步骤,c++有了函数模板。
编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译(进行简单纠错,替换),在调用的时候还会进行一次编译,这次会用实参的类型来取代模板中的虚拟类型,产生具体的函数体,并调用它。
template <typename T>
T max(T &a, T2 &b)
{具体内容}
max<string, int>(a, b); // 显式类型调用
max<>(a, b); // 可以使用空参数列表的方式直接调用函数模板
max('a', a); // 自动数据类型推导.因为模板函数不能隐式转换,但是普通函数可以,因此此处调用的是普通函数
普通函数的优先级高于函数模板,重载时优先调用普通函数 如果可以产生一个更好的匹配,就调用函数模板,空参数也会调用函数模板 模板函数不能隐式转换,但是普通函数可以
函数模板,如果传入的都是同类型,会自动转换获取类型T;如果传入的类型不同,一定要使用声明<类型>放在函数名和()之间;如果传入的类型不同,又没有类型声明,那就出错了
外部模板
传统 C++ 中,模板只有在使用时才会被编译器实例化。也就是说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化,例如,在fun.h定义后, 在use1.cpp和use2.cpp中都使用了该模版函数,则在编译这两个cpp文件时需要分别实例化,这就导致了重复实例化和编译时间的增加, 影响性能。
外部模板扩充了原来的强制编译器在特定位置实例化模板的语法,使得能够显式的告诉编译器何时进行模板的实例化。
//显示实例化方法:
template class|struct 模板名 < 实参列表 > ;
extern template class|struct 模板名 < 实参列表 > (C++11 起);
//如:
//fun.h
template <typename T>
void fun(T t){
}
//use1.cpp
void test1(){
fun<int>(1);
}
//use2.cpp
extern template void fun<int>(int); // 加一行这个防止再次实例化
void test2(){
fun<int>(1);
}
// 尖括号: 传统的>>被当做右移运算符,从C++11开始,连续的右尖括号合法,如:
vector<std::vector<int>> vi;
// 类型别名模板: 传统typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个
// 新的名称,因为模板不是类型。默认模板参数:decltype拖尾返回类型
模板别名
模板是用来产生类型的。在传统 C++ 中,typedef
可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。C++11 使用 using
引入了下面这种形式的写法
template<class T1, class T2>
class att {
//body;
};
template<class T1>
using ylt = att<T1, int>; //using
//main():
ylt<long> num;
变长参数模板
对参数进行解包,有两种经典的处理手法:
//1.递归
template <typename T0>
void printf1(T0 t)
{
cout << t;
}
template <typename T1, typename ...Ts>//d
void printf1(T1 t, Ts...args)//d
{
cout << t;
printf1(args...);//d
}
//2.展开,C++17支持
template <typename T1, typename ...Ts>
void printf1(T1 t, Ts...args)
{
cout << t;
if constexpr (sizeof...(args) > 0) printf1(args...);//d
}
折叠表达式
C++17,对变长参数模板简化
template<typename ... T>
auto printf1(T ... t) {
(std::cout << ... << t) << std::endl;
}
printf1("hello", " ", "world", "!");//hello world!
template<typename ... T>
auto printf1(T ... t) {
return (t + ...);
}
cout << printf1(1, 2, 3, 4);//10
非类型模板参数推导
前面的主要是类型模板参数,其中模板的参数 T
和 U
为具体的类型。 但还有一种模板参数形式可以让不同字面量成为模板参数,即非类型模板参数:
template <auto value> void print2() {
cout << value;
return;
}
int main() {
print2<10>();
print2<"as">();
}
内存管理
在C++中,内存被分成五个区:栈、堆、全局/ 静态存储区、常量区
栈:存放函数的参数和局部变量,实现函数的调用,编译器自动分配和释放。比如存放array,声明一个局部变量int a = 10; 函数结束则销毁。
堆:new/delete,malloc/free,vector动态分配的内存,由程序员分配和释放,没释放的部分程序结束时被OS回收,可动态扩展和收缩。(用malloc函数需要指定内存分配的字节数并且不能初始化对象 ,而new 会自动调用对象的构造函数。delete 会调用对象的destructor ,而free 只是C的标准库函数,只能用于释放通过malloc等函数分配的内存,不会调用对象的destructor)
全局/静态存储区:存放全局变量和静态变量
常量区:存放常量,不允许被修改
代码区:存放函数体的二进制代码
堆栈区别:生长方向,是否产生碎片,管理方式 运行效率:栈是计算机系统提供的数据结构,堆只是由C++函数库提供,OS底层对栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈有专门的指令执行。显然栈的效率比堆高
sizeof:sizeof(类名) = 如果这个类其实啥都没有,记住,什么都没有存的类是默认一个字节的
内存泄露
内存泄漏:动态分配内存所开辟的空间,在使用完毕后未手动释放,导致该内存无法释放(也许丢失了地址),系统也不能再次将它分配给需要的程序
解决:malloc/free要配套,对指针赋值的时候注意被赋值的指针是否需要释放
内存溢出:指程序申请内存时,没有足够的内存供申请者使用
关系:内存泄漏的堆积最终会导致内存溢出
异常处理
C++ 异常处理涉及到三个关键字:检查(try),抛出(throw)和捕捉(catch)。
try 块中的代码标识将被激活的特定异常,catch跟在try后捕获异常,throw 语句可以放在代码的任何地方抛出异常。
try { 被检查的语句}
catch (异常信息类型 [变量名]){
进行异常处理的语句,
可用throw;
}
-
对于大多数 C++ 程序,应使用 C++ 异常处理。 它是类型安全的,可确保在展开堆栈时调用析构函数。
-
Windows 提供自己的异常机制,称为结构化异常处理 (SEH)。 建议不要将该机制用于 C++ 或 MFC 编程。 仅在非 MFC C 程序中使用 SEH。
-
它仍支持其较早的异常处理宏,这些宏在形式上与 C++ 异常类似。
链接库
动态链接库和静态链接库的区别
-
LIB包含了函数所在的DLL文件和文件中函数位置信息(入口),代码由运行时加载在进程空间中的DLL提供,称为动态链接库dynamic link library。
-
LIB包含函数代码本身,在编译时直接将代码加入程序当中,称为静态链接库static link library。
-
(所以无论是动态链接库还是静态链接库,都会有lib文件
静态链接库的使用
需要的文件: 头文件 .h 、静态库 .lib 头文件.h中有函数的声明,使用静态链接库的项目需要引用该文件才能编译通过 .lib包含了实际执行代码、符号表等等 加载lib的方法: 法1.使用编译链接参数或者VS的配置属性来设置 法2.使用pragma编译语句,例如pragma comment(lib,"a.lib") .lib中的指令将全部被直接包含在最终生成的 EXE 文件中
动态链接库的使用
一、动态库的隐式调用:
需要的文件: 头文件 .h 、动态链接库的.lib文件,动态链接库的dll文件 头文件.h和静态链接库使用时的作用一样,使用动态链接库中的函数的项目需要引用该文件才能编译通过
.lib包含了函数所在的DLL文件和文件中函数位置的信息,.dll包含了实际执行代码、符号表等等 加载lib的方法:lib是编译链接是用的,跟使用静态链接库时一样有两种方法:法1.使用编译链接参数或者VS的配置属性来设置 法2.使用pragma编译语句,例如pragma comment(lib,"a.lib") 加载dll的方法:dll是运行时用的,链接了lib之后形成的EXE可执行文件中已经有了dll的信息,所以只要把dll放在和exe同一个目录下就可以了,运行时根据EXE需要自动加载dll中的函数
二、动态库的显式调用:
需要的文件: 动态链接库的dll文件 不需要.h头文件和.lib文件,因为LoadLibrary之后可以使用getProcAddress来查找一个函数的地址从而调用该函数 (显式调用的前提是使用者需要知道想调用的函数的名字、参数、返回值信息,也就是说虽然编译链接用不上.h头文件,但是调用者编程时可能还是要看.h文件作参考来知道函数名字、参数、返回值信息)
C++代码规范
命名规范
-
通用:函数命名,变量命名,文件命名要有描述性;少用缩写
-
文件:全部小写, 可以包含下划线 (
_
) 或连字符 (-
) -
类型:类、结构体、类型定义 (
typedef
)、枚举,每个单词首字母均大写,不包含下划线 -
变量:全部小写,单词间可用下划线连接(也可以无);类的成员变量以下划线结尾,结构体的不用;全局变量没有特别要求, 少用就好, 也可以用
g_
或其它标志作为前缀来区分 -
常量:在全局或类里的常量名称前加
k
,且k
之外每个单词开头字母均大写 -
函数:
-
常规函数:每个单词首字母大写,没有下划线;短小的内联函数名也可以用小写字母
-
取值设值函数:要与存取的变量名匹配,有下划线
-
-
命名空间:小写字母,具体取决于项目名称,要避免嵌套的命名空间与常见的顶级命名空间发生名称冲突
-
宏:通常不使用宏,如必须,全部大写,使用下划线,如#define PI_CIRCLE 3.14
-
特例:如果与已有 C/C++ 实体相似,可参考现有命名策略
头文件
-
函数参数顺序:参数分为输入参数和输出参数两种,将所有输入参数置于输出参数之前
-
包含文件的名称及次序:C 库、C++库、其他库的.h、项目内的.h,增强可读性、避免隐藏依赖
作用域
-
全局变量:class 类型的全局变量是被禁止的(包括STL的string、vector等,因为它们的初始化顺序可能会导致出现问题),内建类型的全局变量是允许的,当然多线程代码中非常数全局变量也是被禁止的
C++类
-
构造函数的职责:构造函数中只进行那些没有实际意义的初始化,可能的话,使用Init()方法集中初始化为有意义(non-trivial)的数据
-
拷贝构造函数:仅在代码中需要拷贝一个类的对象的时候使用拷贝构造函数,不需要拷贝时使用DISALLOW_COPY_AND_ASSIGN这个宏显式的禁用它们
-
继承:在实际开发中,尽量多用组合少用继承;重定义派生的虚函数时,在派生类中也要声明其为virtual
-
多重继承:虽然允许,但是只能一个基类有实现,其他基类是接口;C++过于灵活也是个麻烦的问题,只能通过代码规范填坑
-
接口:虚基类必须以Interface为后缀,方便阅读
-
重载操作符:除少数特定情况外,不要重载操作符,虽然好用但出bug了容易找不到
-
声明次序:
1)typedefs 和 enums; 2)常量; 3)构造函数; 4)析构函数; 5)成员函数,含静态成员函数; 6)数据成员,含静态数据成员; 7)宏 DISALLOW_COPY_AND_ASSIGN 置于private块之后,作为类的最后部分
其它特性
-
引用参数:函数形参表中,所有的引用必须得const
-
缺省参数:禁止使用函数缺省参数
-
异常:不要使用 C++异常
-
流:除了记录日志,不要使用流,使用printf之类的代替
-
const使用:在任何可以的情况下都要使用const
统一类型转换机制
要求:1.统一风格 2.精度丢失 3.失败的异常处理
借鉴:boost库中conversion库已经为这个问题提供了一整套的解决方案
步骤:
-
学习boost的conversion库,完善测试用例
-
结合我们的系统整理我们自己的转换api
-
重构现代码,采用统一的类型转换api