C++语言
- 0. 课程目标
- 1. 头文件
- 2. 预处理器
- 3. 关键字
- 3.1 new/delete
- 3.2 template/typename/typedef/using
- 3.3 decltype/auto
- 3.4 volatile/mutable/const/constexpr
- 3.5 static
- 3.6 inline
- 3.7 const_/static_/reinterpret_/dynamic_cast
- 3.8 private/protected/public/friend
- 3.9 virtual/override/final
- 3.10 =delete/=0/default/explicit
- 3.11 struct/class/this
- 3.12 void/void\*/nullptr (C++11 起)
- 4. 表达式
- 5. 申明
- 6. 初始化
- 7. 函数
- 8. 类
- 9. 模板
0. 课程目标
学多深?以被面试官问到的深度为参考。
按照cppreference中对c++知识的梳理,本材料依照下图中的树形结构分门别类记录知识点。

其中黄框内的特性是c++11起支持的,将在这个博客中单独呈现;
其中容器库内容很多,将在这个博客中单独呈现;
所以本博客主要学习语言和头文件部分;
头文件的内容很少,不单独做大标题,放在基本概念中;
本文仅包含下列内容:

1. 头文件
c++中头文件引入方式是尖括号不带后缀,例如: 等等,因为系统头文件也会互相引用,因此不必include全,一般用clion等IDE也会自动提示。
对于一些形式为 xxx.h 的 C 标准库头文件,C++ 标准库同时包含同名的头文件和拥有形式 cxxx 的头文件。
尖括号or双引号?
- 使用 “xxx.h”,告诉编译器,从当前工作目录开始查找;
- 使用<xxx.h>,告诉编译器,从系统默认目录中去查找;
- 若 #include “” 查找成功,则遮蔽 #include <> 所能找到的同名文件;
搜索顺序?
- 当前目录
- -I指定的目录
- gcc的环境变量CPLUS_INCLUDE_PATH(C程序使用的是C_INCLUDE_PATH)
- gcc的内定目录:/usr/include,/usr/local/include,/usr/lib/gcc/x86_64-redhat-linux/4.1.1/include
用户头文件的编写要注意防御式申明,防止头文件反复引用,官方的称呼是“Conditional inclusion”。
// xx.h
#ifndef __SOMETHING__
#define __SOMETHING__
...
#endif
2. 预处理器
预处理器在编译前的翻译阶段执行。
- 有条件编译源文件的某些部分(由 #if、#ifdef、#ifndef、#else、#elif 和 #endif 指令控制); 编译开关
- 替换文本宏,同时可能对标识符进行拼接或加引号(由 #define 和 #undef 指令与 # 和 ## 运算符控制); 函数工厂
- 包含其他文件(由 #include 指令控制);
- 由实现定义的行为(由 #pragma 指令控制);
2.1 define宏
#define 标识符 替换列表(可选) (1)
#define 标识符( 形参, … ) 替换列表(可选) (C++11 起) (2)
(1)仿对象宏,注意#define只是简单机械的替换,注意展开后的语义,尽量加括号保护
(2)仿函数宏,以标识符中的括号为特征
2.2 标识符拼接
字符串化
仿函数宏中,如果替换列表 中一个标识符前有 # 运算符,则该标识符在运行形参替换的基础上以引号包围,实际上创建一个字符串字面量。另外,预处理器为内嵌的字符串字面量(若它存在)外围的引号添加反斜杠以进行转义,并按需要双写字符串中的反斜杠。移除所有前导和尾随空白符,并将文本中间(除内嵌字符串字面量中间外)的任何空白符序列缩减成单个空格。此操作被称为“字符串化”。
另外,#@表示加单引号;
#define showlist(...) puts(#__VA_ARGS__)
showlist(); // 展开成 puts("")
showlist(1, "x", int); // 展开成 puts("1, \"x\", int")
标识符拼接
如果 替换列表 中任何两个相继标识符之间有 ## 运算符,则这两个个标识符(首先未被宏展开)在运行形参替换的基础上将结果进行拼接。此操作被称为“拼接”或“记号粘贴”。
// 制造函数工厂并使用它
#define FUNCTION(name, a) int fun_##name() { return a;}
2.3 pragma
优点:头文件标准防护中,必须保重宏名唯一,#pragma once使得错误地在多个文件中使用相同的宏名变得不可能。
缺点:#pragma once 的文件是基于其文件系统层次的身份所排除的,所以若头文件在项目中有多个位置,则这不能防止包含它两次。
3. 关键字
3.1 new/delete
c中malloc()、freee()是函数,分配堆内存;
c++中new、delete是操作符;分别进行2步操作:
1.内存的分配,2.构造函数的调用;
3.析构函数的调用,4.内存的销毁。
void* operator new(size_t size);
void* operator new[](size_t size);
void operator delete(void *p);
void operator delete[](void *p);
简言之,new和delete不仅承载着内存分配的功能还承载着对象构造/析构函数的调用功能。
new和delete对内存分配的功能实际上还是由调用malloc()/free()实现。
c++中delete和delete[]要注意甄别,申请内存数组时,new type[num]会记住申请了几个对象,delete[]会据此多次调用析构函数。举个栗子。
3.2 template/typename/typedef/using
template
模板是==泛型编程==的基础,例如我们常使用的vector a;就是用int类型实例化类模板。
常见用法:函数模板、类模板;
template<class T>
template<typename T>
两者没有本质区别,早期没有typename关键字,现在更推荐用typename;
T只是占位符,用其他名称代替也行。
类模板,即class中有些数据类型用T指代:
template<typename T>
class A {
public:
A(T r = 0) : re(r) {}
private:
T re;
...
};
A<double> a1(0.4);
函数模板,编译器会自动进行实参推导(argument deduction);
stone r1(2,3), r2(3,4), r3;
r3 = min(r1, r2);
template<class T>
inline const T& min(const T&a, const T&b) {return b < a ? b : a;}
typedef
创建能在任何位置替代(可能复杂的)类型名的别名。
// 避免必须写 "struct C" 的常见 C 手法
typedef struct {int a; int b;} S, *pS;
using/namespace
namespace包含系统库文件内置的命名空间、用户自定义的命名空间;
c++11起支持inline命名空间;
using 指令:using namespace spaceX;
从 using 指令之后到指令出现的作用域结尾为止,命名空间spaceX内的任何名字均可见。
using 声明:using spaceX::nameY;
令来自命名空间 spaceX 的符号 nameY 对无限定名字查找可见。
using 别名 = 原先类型;
using还可以做到:alias templates,比typedef更强大,也更直观。
3.3 decltype/auto
decltype (C++11 起)
检查实体的声明类型,或表达式的类型和值类别。
若实参为无括号的标识表达式或无括号的类成员访问表达式,则 decltype 产生以此表达式命名的实体的类型。
如果对象的名字带有括号,则它被当做通常的左值表达式,从而 decltype(x) 和 decltype((x)) 通常是不同的类型。
decltype(a->x) y; // y 的类型是 double(其声明类型)
decltype((a->x)) z = y; // z 的类型是 const double&(左值表达式)
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) // 返回类型依赖于模板形参
{ // C++14 开始可以推导返回类型
return t+u;
}
auto (C++11 起)
类型说明占位符。
用模板实参推导的规则推导类型。
占位符 auto 可伴随如 const 或 & 这样的修饰符,它们参与类型推导。
auto a = 1 + 2; // a 的类型是 int
decltype(auto) c1 = a; // c1 的类型是 int,保有 a 的副本
decltype(auto) c2 = (a); // c2 的类型是 int&,为 a 的别名
// auto 常用于无名类型,例如 lambda 表达式的类型
auto lambda = [](int x) { return x + 3; };
auto会忽略引用,其次,auto一般会忽略掉顶层const,但底层const会被保留下来,比如当初始值是一个指向常量的指针时:
int i = 0;
const int ci = i, &cr = ci; //ci 为整数常量,cr 为整数常量引用
auto a = ci; // a 为一个整数, 顶层const被忽略
auto b = cr; // b 为一个整数,顶层const被忽略
auto c = &ci; // c 为一个整数指针.
auto d = &cr; // d 为一个指向整数常量的指针(对常量对象区地址是那么const会变成底层const)
如果你希望推断出auto类型是一个顶层的const,需要明确指出:
const auto f = ci; // f 为 const int类型
decltype与auto的区别
decltype在处理顶层const和引用的方式与auto有些许不同,如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。
decltype与auto的联系
// 在其所调用的函数返回引用的情况下
// 函数调用的完美转发必须用 decltype(auto)
template<class F, class... Args>
decltype(auto) PerfectForward(F fun, Args&&... args)
{
return fun(std::forward<Args>(args)...);
}
3.4 volatile/mutable/const/constexpr
volatile
adj. [化学] 挥发性的;不稳定的;爆炸性的;反复无常的
const 与 volatile统称为“CV”类型限定符。
1.易变性:volatile告诉编译器,某个变量是易变的,当编译器遇到这个变量的时候,只能从变量的内存地址中读取这个变量,不可以从寄存器、或者其它任何地方读取。
2.顺序性:两个包含volatile变量的指令,编译后不可以乱序。注意是编译后不乱序,但是在执行的过程中还是可能会乱序的,这点需要由其它机制来保证,例如memory- barriers(带有lock前缀的指令)。
volatile atomic lock 对比?
mutable
容许在即便包含它的对象被声明为 const 时仍可修改声明为 mutable 的类成员。
const struct
{
int n1;
mutable int n2;
} x = {0, 0}; // 带 mutable 成员的 const 对象
// x.n1 = 4; // 错误:const 对象的成员为 const
x.n2 = 4; // ok,const 对象的 mutable 成员不是 const
const
const指定该类型的对象不可修改;
非静态成员函数是声明于类的成员说明中,不带 static 或 friend 说明符的函数。
仅可用于非静态成员函数的额外语法元素:final 与 override 说明符,纯说明符(=0),cv 限定符,引用限定符,以及成员初始化列表。
constexpr (C++11 起)
constexpr 说明符声明可以在编译时求得函数或变量的值。 允许程序利用编译时的计算能力,也有编译变慢的代价。
然后这些变量和函数(若给定了合适的函数实参)即可用于仅允许编译时常量表达式之处。
用于对象或非静态成员函数 (C++14 前)声明的 constexpr 说明符蕴含 const。
用于函数声明的 constexpr 说明符或 static 成员变量 (C++17 起)蕴含 inline。
若函数或函数模板的任何声明拥有 constexpr 说明符,则每个声明必须都含有该说明符。
3.5 static
用来控制变量的存储方式和可见性。
静态成员函数在class内部申明,但必须在class外部定义。
static 关键词仅与静态成员在类定义中的声明一同使用,但不与该静态成员的定义一同使用;
静态成员函数不能为 virtual、const 或 volatile。
静态数据成员不能为 mutable。
静态数据成员可以声明为 inline。 inline 静态数据成员可以在类定义中定义,而且可以指定初始化器。它不需要类外定义。
静态数据成员可以声明为 const(且非 volatile)。它可以直接在类定义内初始化,也可以在类外定义。
合理的单例模式:
class A {
public:
static A& getInstance();
setup() {...}
private:
A(); //构造
A(const A& rhs); //拷贝构造
...
};
A& A::getInstance()
{
static A a; //被调用时才唯一一次创建
return a;
}
3.6 inline
内联函数,在函数调用的地方,编译器会直接替换成函数体,减少因函数调用产生的开销。
完全在 class/struct/union 的定义之内定义的函数,无论它是成员函数还是非成员 friend 函数,均为隐式的内联函数。
声明有 constexpr 的函数是隐式的内联函数。
这一点功能上类似于宏展开,但是比宏展开好的地方是,inline发生在编译阶段,会做类型检查,消除了宏展开可能带来的语义隐患。
3.7 const_/static_/reinterpret_/dynamic_cast
c++类型转换分为显式类型转换和隐式类型转换。
隐式类型转换
凡是在语境中使用了某种表达式类型 T1,但语境不接受该类型,而接受另一类型 T2 的时候,会进行隐式转换(T1转换为T2类型),具体是:
- 调用以 T2 为形参声明的函数时,以该表达式为实参;
- 运算符期待 T2,而以该表达式为操作数;
- 初始化 T2 类型的新对象,包括在返回 T2 的函数中的 return 语句;
- 按语境转换
将表达式用于 switch 语句(T2 为整型类型);
将表达式用于 if 、while、for控制、:首个操作数、逻辑运算(T2 为 bool)。
delete 表达式的实参(T 是任何对象指针类型)。 - 数组到指针转换
- 数值提升
仅当存在一个从 T1 到 T2 的无歧义隐式转换序列时,程序良构(能编译)。
显式类型转换
遇到 C 风格转型表达式时,C++编译器尝试将它解释成下列转型表达式,以此顺序:const_/static_/reinterpret_cast。
为什么要设计成C++风格的显式类型转换?编译器优化+规范程序员。
const_cast
语法:A = const_cast(B)
这个运算符可以用来去除一个对象的const或volatile属性,但仍无法修改原值。typeA必须是一个指针或者引用。
static_cast
是最常用到的转化操作符,使用它可以消除因产生类型转化而可能产生的编译器warnings,static_cast全部用于明确定义的变换,包括编译器允许我们做的不用强制转换的“安全”变换和不太安全但清楚定义的变换。static_cast包含的转化类型包括典型的非强制类型转换、窄化变化(会有信息丢失)、void*与T*转换、隐式类型变换和类层次的静态定位(基类和派生类之间的转换)。
dynamic_cast
语法:A=dynamic_cast(B)
TypeA必须是类的指针、类的引用或者void *;
dynamic_cast一般只在继承类对象的指针之间或引用之间进行类型转换。如果没有继承关系,则被转化的类具有虚函数对象的指针进行转换。
reinterpret_cast
语法:A = reinterpret_cast(B)
这是一种最不安全的转换,最有可能出现问题, 不考虑前后是否具备转换的条件(如A*与B*,int与指针…)。
#include <iostream>
class Complex{
public:
Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
public:
operator double() const { return m_real; } //类型转换函数
private:
double m_real;
double m_imag;
};
int main(){
Complex c(12.5, 23.8);
//下面是正确的用法
double real= static_cast<double>(c); //调用类型转换函数
//下面的用法是错误的
//float *p3 = static_cast<float*>(p1); //不能在两个具体类型的指针之间进行转换
//p3 = static_cast<float*>(0X2DF9); //不能将整数转换为指针类型
return 0;
}
3.8 private/protected/public/friend
带继承情况下,组合结果为基类保护级别和继承方式中级别更高者(级别private>protected>public)。
private
只能由1.该类中的函数、2.其友元函数访问。
不能被任何其他访问,该类的对象也不能访问。
protected
可以被1.该类中的函数、2.其友元函数、3.子类的函数访问。
但不能被该类的对象访问。
public
可以被1.该类中的函数、2.其友元函数、3.子类的函数、4.该类的对象访问。
friend
友元函数包括3种:设为友元的普通的非成员函数;设为友元的其他类的成员函数;设为友元类中的所有成员函数。
友元关系不对称(我说你是朋友,那你可以来访问我;但我不能访问你)。
友元关系不传递(你朋友的朋友不是你的朋友)。
友元关系不继承(你朋友的孩子不是你的朋友)。
3.9 virtual/override/final
virtual 说明符之虚基类
对于每个指定为 virtual 的不同基类,最终派生对象中仅含有该类型的一个基类子对象。多级虚继承时每次都用virtual指定,最终才会只有一份基类。
virtual 说明符之虚函数
指定非静态成员函数为虚函数并支持动态调用派发。
当使用基类的指针或引用来处理派生类时,对被覆盖的虚函数的调用,将会调用定义于派生类中的行为。这种函数调用被称为虚函数调用或虚调用。当使用有限定名字查找(即函数名出现在作用域解析运算符 :: 的右侧)时,虚函数调用被抑制。
若某个成员函数 vf 在类 Base 中被声明为 virtual,且某个直接或间接派生于 Base 的类 Derived 拥有一个下列几项与之相同的成员函数声明:函数名、形参列表(但非返回类型)、cv 限定符、引用限定符,则类 Derived 中的此函数亦为虚函数(无论其声明中是否使用关键词 virtual)并覆盖 Base::vf(无论其声明中是否使用单词 override)。
具有相同名字但不同形参列表的函数并不覆盖同名的基类函数,但会隐藏它:在无限定名字查找检查派生类的作用域时,查找找到该声明而不再检查基类。
struct B { virtual void f(); };
struct D : B { void f(int); // D::f 隐藏 B::f(错误的形参列表)};
struct D2 : D { void f(); // D2::f 覆盖 B::f(它不可见也不要紧)};
int main()
{
B b; B& b_as_b = b;
D d; B& d_as_b = d; D& d_as_d = d;
D2 d2; B& d2_as_b = d2; D& d2_as_d = d2;
b_as_b.f(); // 调用 B::f()
d_as_b.f(); // 调用 B::f()
d2_as_b.f(); // 调用 D2::f()
d_as_d.f(); // 错误:D 中的查找只找到 f(int)
d2_as_d.f(); // 错误:D 中的查找只找到 f(int)
}
要覆盖的 Base::vf 不需要可见(可声明为 private,或用私有继承继承)。
每个虚函数都有其最终覆盖函数,它是进行虚函数调用时所执行的函数。
虚析构函数
虽然析构函数是不继承的,但若基类声明其析构函数为 virtual,则派生的析构函数始终覆盖它。这使得可以通过指向基类的指针 delete 动态分配的多态类型对象。
override说明符
指定一个虚函数覆盖另一个虚函数。
区别于重载,重载指函数标识符不完全相同;
final说明符
指定某个虚函数不能在子类中被覆盖,或者某个类不能被子类继承。
3.10 =delete/=0/default/explicit
=0
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。
称带有纯虚函数的类为抽象类。
=delete弃置函数
delete除了内存管理的功能外,还具有弃置函数的功能。
default
default除了switch语句中的兜底功能外,还具有令编译器为某class生成特殊成员函数(big six)的功能。
explicit 说明符
指定构造函数或转换函数为显式,即,它不能用于隐式转换和复制初始化。
只可出现于在类定义之内的构造函数或转换函数 (C++11 起)的 声明说明符序列 中。
只有一个不带默认值的形参的构造函数,按规范来写,必须带上explicit说明符。
默认构造函数
复制构造函数
移动构造函数
复制赋值运算符
移动赋值运算符
析构函数
3.11 struct/class/this
C++中的 struct 和 class 基本是通用的,唯有几个细节不同:
- 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的
- class 继承默认是 private 继承,而 struct 继承默认是 public 继承
- class 可以使用模板,而 struct 不能
this指针
class A{
int x;
public:
void setA(int x) {
this->x = x; // 如果用x=x,则仍然值没有变
}
};
若成员函数有 cv 限定,则 this 的类型是 cv X*(指向同一 cv 限定的 X 的指针)。
3.12 void/void*/nullptr (C++11 起)
void
是不完整类型,可以作为函数返回值类型,可以有指向void的指针void*,可以指示函数不接受形参。
nullptr
是指针字面量,存在从 nullptr 到任何指针类型及任何成员指针类型的隐式转换。
NULL是宏:
#define NULL 0
#define NULL nullptr // C++11 起
4. 表达式
4.1 值类别

lvalue, 左值指明一个函数或对象。举例而言,如果E是指针类型的表达式,那么*E就是指向了对象或函数的一个左值。另一个例子是,调用一个返回值是左值的结果依旧是一个左值。
xvalue, 它也指向一个对象,通常已经接近了它生存期的结束。(这样一来,它的资源即将被移动)xvalue是包含了右值引用特定类型表达式的结果。(例子:调用返回值为右值引用的结果是一个xvalue)
glvalue, generalized lvalue, 是指更泛化意义上的左值,包含了以前的一部分右值。即lvalue和xvalue的集合。
rvalue, 可以是一个xvalue, 一个临时对象,或临时对象的子对象,或者是一个没有和对象关联的值。
prvalue, pure right value, 纯右值,即不是xvalue的rvalue (举例:调用函数返回值并不是引用类型的结果是一个纯右值。字面量12.7, 3e5, 或者true, 都是纯右值。)
特殊表达式
static_cast 转换一个类型为另一相关类型
dynamic_cast 在继承层级中转换
const_cast 添加或移除 cv 限定符
reinterpret_cast 转换类型到无关类型
C 风格转型 以 static_cast 、 const_cast 及 reinterpret_cast 的混合转换一个类型到另一类型
new 创建有动态存储期的对象
delete 销毁先前由 new 表达式创建的对象,并释放其所拥有的内存区域
sizeof 查询类型的大小
sizeof… 查询形参包的大小(C++11 起)
typeid 查询类型的类型信息
noexcept 查询表达式是否能抛出异常(C++11 起)
alignof 查询类型的对齐要求(C++11 起)
5. 申明
5.1 引用
引用
- 引用必须被初始化为指代一个有效的对象或函数。
- 不存在 void 的引用,也不存在引用的引用。
- 引用类型无法在顶层被 cv 限定;声明中没有为此而设的语法,而若将限定性添加到 typedef 名、 decltype 说明符或类型模板形参,则忽略它。
- 引用不是对象;它们不必占用存储。
- 因为引用不是对象,故不存在引用的数组,不存在指向引用的指针,不存在引用的引用:
& 与 &&
分别为左值引用和右值引用;
左值引用用于建立既存对象的别名,亦可用于在函数调用中实现按引用传递语义;
右值引用可用于为临时对象延长生存期(注意,左值引用亦能延长临时对象生存期,但不能通过左值引用修改它们);
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
std::string& r2 = s1 + s1; // okay:到 const 的左值引用延长生存期
// r2 += "Hjj"; // 错误:不能通过左值引用修改
std::string&& r3 = s1 + s1; // okay:右值引用延长生存期
r3 += "Test"; // okay:能通过到非 const 的引用修改
更重要的是,当函数同时具有右值引用和左值引用的重载时,右值引用重载绑定到右值(包含纯右值和亡值),而左值引用重载绑定到左值:
void f(int& x) {
std::cout << "lvalue reference overload f(" << x << ")\n";
}
void f(const int& x) {
std::cout << "lvalue reference to const overload f(" << x << ")\n";
}
void f(int&& x) {
std::cout << "rvalue reference overload f(" << x << ")\n";
}
int main() {
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&)
// 若不提供 f(int&&) 重载则会调用 f(const int&)
f(std::move(i)); // 调用 f(int&&)
// 右值引用变量在用于表达式时是左值
int&& x = 1;
f(x); // 调用 f(int& x)
f(std::move(x)); // 调用 f(int&& x)
}
右值引用与move
右值引用的性质允许在适当时机自动选择移动构造函数、移动赋值运算符和其他具移动能力的函数(例如 std::vector::push_back())。
因为右值引用能绑定到亡值,故它们能指代非临时对象:
int i2 = 42;
int&& rri = std::move(i2); // 直接绑定到 i2
std::cout<<i2<<" "<<rri<<std::endl; // 42 42
i2 = 300; // 注意:虽然用了std::move,i2还能接着用
std::cout<<i2<<" "<<rri<<std::endl; // 300 300
rri = 999;
std::cout<<i2<<" "<<rri<<std::endl; // 999 999
6. 初始化
7. 函数
7.1 Lambda表达式
构造闭包:能够捕获作用域中的变量的无名函数对象。
3584



被折叠的 条评论
为什么被折叠?



