「生活可以更简单, 欢迎来到我的开源世界」
  1. C++
    1. const
    2. static
    3. this指针
    4. 类的静态成员
    5. inline
    6. 虚函数可以是内联函数吗?
    7. volatile(C++11以后)
    8. assert()
    9. 数组和指针的区别
    10. 指针和引用的区别
      1. C++为什么有了指针还要引入引用?
      2. 既然引入了引用,为何不抛弃指针呢?
    11. sizeof和strlen的区别
      1. 语法
      2. 区别
      3. 进阶
    12. #pragma pack(n)
    13. 位域
    14. extern “C”
    15. struct 和 typedef struct
    16. C++ 中 struct 和 class
    17. union 联合
    18. C 实现 C++ 类
    19. explicit(显式)关键字
    20. friend 友元类和友元函数
    21. using
      1. using 声明
      2. using 指令
    22. :: 范围解析运算符
    23. enum 枚举类型
    24. decltype
    25. 引用
      1. 左值引用
      2. 右值引用
      3. 引用折叠
    26. 成员初始化列表
    27. initializer_list 列表初始化
    28. 面向对象
    29. 封装
    30. 继承
    31. 多态
      1. 静态多态(编译期/早绑定)
      2. 动态多态(运行期期/晚绑定)
    32. 虚析构函数
    33. 纯虚函数
    34. 虚函数、纯虚函数
    35. 虚函数指针、虚函数表
    36. 虚继承
    37. 虚继承、虚函数
    38. 模板类、成员模板、虚函数
    39. 抽象类、接口类、聚合类
    40. 内存分配和管理
      1. malloc、calloc、realloc、alloca
      2. malloc、free
      3. new、delete
      4. placement new
      5. new与malloc的区别,delete和free的区别
    41. delete this 合法吗?
    42. 如何定义一个只能在堆上(栈上)生成对象的类?
      1. 只能在堆上
      2. 只能在栈上
    43. 智能指针
      1. C++ 标准库(STL)中
      2. C++ 98
      3. C++ 11(实际意义上的智能指针)
        1. auto_ptr
        2. auto_ptr 与 unique_ptr 比较
    44. 强制类型转换运算符
      1. bad_cast
    45. 运行时类型信息 (RTTI)
      1. dynamic_cast
      2. typeid
      3. type_info
  2. Effective C++
  3. More Effective C++
  4. Google C++ Style Guide
  5. 其他
  6. 数据结构
  7. 算法(Algorithm)
    1. 递归
      1. 递归与分治
      2. 递归与迭代
    2. 排序
    3. 查找
    4. 图搜索
    5. 算法策略
  8. 操作系统
  9. 计算机网络
  10. Unix网络编程
  11. 数据库
    1. 基本概念
    2. 常用数据模型
    3. 常用 SQL 操作
    4. 关系型数据库
      1. 索引
    5. 数据库完整性
    6. 关系数据理论
      1. 范式
    7. 数据库恢复
    8. 并发控制
  12. 设计模式
    1. 设计模式的六大原则
  13. 链接装载库
    1. 内存、栈、堆
      1. “段错误(segment fault)” 或 “非法操作,该内存地址不能 read/write”
    2. 编译链接
      1. 各平台文件格式
      2. 编译链接过程
      3. 目标文件
        1. 目标文件格式
        2. 目标文件存储结构
      4. 链接的接口————符号
    3. Linux 的共享库(Shared Library)
      1. 命名
      2. 路径
      3. 环境变量
      4. so 共享库的编写
      5. so 共享库的使用(被可执行项目调用)
    4. Windows 应用程序入口函数
    5. Windows 的动态链接库(Dynamic-Link Library)
      1. 用处
      2. 注意
      3. 加载 Windows 程序的搜索顺序
      4. DLL 入口函数
      5. 载入卸载库
      6. 显示地链接到导出符号
      7. DumpBin.exe 查看 DLL 信息
      8. LoadLibrary 与 FreeLibrary 流程图
        1. LoadLibrary
        2. FreeLibrary
      9. DLL 库的编写(导出一个 DLL 模块)
      10. DLL 库的使用(运行时动态链接 DLL)
    6. 运行库(Runtime Library)
      1. 典型程序运行步骤
      2. glibc 入口
      3. MSVC CRT 入口
      4. C 语言运行库(CRT)
      5. C语言标准库(ANSI C)
  14. 书籍
    1. 语言
    2. 算法
    3. 系统
    4. 网络
    5. 其他
  15. C/C++发展方向
    1. 后台/服务器
    2. 桌面客户端
    3. 图形学/游戏/VR/AR
    4. 测试开发
    5. 网络安全/逆向
    6. 嵌入式/物联网
    7. 音视频/流媒体/SDK
    8. 计算机视觉/机器学习
C/C++ 技术面试基础知识总结
2020-12-30

改自:C/C++ 技术面试基础知识总结

遵循 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享) 协议,转载请注明出处,不得用于商业目的。

C++

const

作用:

  1. 修饰变量,说明该变量不可以被改变
  2. 修饰指针
    • 指向常量的指针(pointer to const)
    • 自身是常量的指针(常量指针,const pointer)
  3. 修饰引用
    • 指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改
    • 没有本身是常量的引用(const reference),const修饰对象,引用本身不是对象【引用本身就是 const pointer】
  4. 修饰成员函数,说明该成员函数内不能修改成员变量
//函数中使用const
void func(const int a);//修饰函数参数值,无意义,形参是临时值
void func(const char* a);//修饰参数值指向对象,所指内容不可变
void func(char* const a);//修饰函数参数值,无意义,形参是临时值
void func(const int& a); //引用参数在函数内为常量,不可变
/*
const引用传递和最普通的函数按值传递(不加任何修饰)的效果是一模一样的
区别是:按值传递会先建立一个类对象的副本,然后传递过去,而引用参数直接传递实参的地址,比按值传递更高效。
可避免拷贝构造。
*/

static

作用:改变生命周期或者改变作用域

1、作用于变量

程序的局部变量存在于堆栈中,全局变量存在于静态区中,动态申请数据存在于中。

  1. 修饰局部变量,使变量成为静态的局部变量,存储在静态区,编译时就为变量分配内存,直到程序退出才释放存储单元。如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  2. 修饰外部变量,外部变量指在所有代码块{}之外定义的变量,它缺省为静态变量,编译时分配内存,程序结束时释放内存单元。外部变量的作用域很广,整个文件都有效甚至别的文件也能引用它。为了限制某些外部变量的作用域,使其只在本文件中有效,而不能被其他文件引用,可以用static关键字对其作出声明。
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。类的静态成员{变量,函数}与类本身直接相关,而不是与类的各个对象保持关联。

总结:用static声明局部变量,使其变为静态存储方式(静态数据区),作用域不变;用static声明外部变量,其本身就是静态变量,这只会改变其连接方式,使其只在本文件内部有效,而其他文件不可连接或引用该变量。

2、作用于函数

  1. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
  2. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员
  3. 类的静态成员存在于任何对象之外,对象中不包含任何于静态数据成员有关的数据。

this指针

  1. this 指针是一个特殊指针,作为每一个非静态成员函数的隐含参数(第一个参数),它指向成员函数所属的对象
  2. 当一个成员函数被调用时,除了显示传递的参数以外,还包含一个隐含参数: this 指针,该参数是一个指向这个成员函数所在的对象的指针,编译程序时将对象的地址赋给 this 指针
  3. 每次成员函数存取数据成员时,都是通过隐式使用this 指针访问数据成员
  4. this指针的隐式声明形式
    • 普通成员函数:ClassName *const this,这意味着不能给 this 指针赋值
    • const 成员函数:const ClassName* const, 这意味着this 指针所指向的对象是不可修改的(即不能对这种对象的数据成员进行赋值操作)
    • static成员函数:静态成员函数也不与任何对象绑定在一起,故而不包含this指针
  5. this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。
  6. 在以下场景中,经常需要显式引用 this 指针:
    1. 为实现对象的链式引用;
    2. 为避免对同一对象进行赋值操作;
    3. 在实现一些数据结构时,如 list

类的静态成员

【详见C++ Primer 第7章笔记】

静态成员函数与类本身直接相关,不与任何对象绑定在一起,它们不包含this指针。

静态成员可以是public的或private的。

类使用作用域运算符::访问静态成员(Account::rate()),成员函数不用通过域运算符就能直接使用静态成员。

静态成员函数不能声明成const的,而且我们不能在static函数体内使用this指针。

类的静态成员存在于任何对象之外,对象中不包含任何于静态数据成员有关的数据。静态成员虽然不属于某个对象,但是仍然可以使用类的对象、引用或者指针来访问静态成员

static关键字只能出现在类内部:类内部声明时使用static,如果类外部定义不需要static声明

静态成员可以是不安全类型

静态成员的类型可以就是它所属的类类型,而非静态成员只能声明成它所属的类的指针或引用。

静态成员和普通成员的另一个区别是我们可以使用静态成员作为默认实参

inline

【详见文章:inline 说明符】

C++17之前:只有内联函数,含义是“优先内联”,即给编译器提意见说想要内联但编译器不一定理你

C++17以后:多了内联变量,含义是“容许多次(一致性)定义”,仍然是非强制性的

特征:

编译器对 inline 函数的处理步骤:

  1. 将 inline 函数体复制到 inline 函数调用点处;
  2. 为所用 inline 函数中的局部变量分配内存空间;
  3. 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
  4. 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。

优点

  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  3. 在类中声明同时定义的成员函数,自动转化为内联函数,(是这个因果吗?)因此内联函数可以访问类的成员变量,宏定义则不能。
  4. 内联函数在运行时可调试,而宏定义不可以。

缺点

  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

虚函数可以是内联函数吗?

Are “inline virtual” member functions ever actually “inlined”?

虚函数内联使用

#include <iostream>  
using namespace std;
class Base
{
public:
inline virtual void who()
{
cout << "I am Base\n";
}
virtual ~Base() {}
};
class Derived : public Base
{
public:
inline void who() // 不写inline时隐式内联
{
cout << "I am Derived\n";
}
};

int main()
{
// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
Base b;
b.who();

// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
Base *ptr = new Derived();
ptr->who();

// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
delete ptr;
ptr = nullptr;

system("pause");
return 0;
}

volatile(C++11以后)

assert()

断言,是一个宏,不是函数。

assert 宏的原型定义在 <assert.h>(C)、<cassert>(C++)中,其作用是如果它的条件返回错误,则终止程序执行。

频繁的调用assert会极大的影响程序的性能,增加额外的开销。在调试结束后,可以通过在包含#include <assert.h>的语句之前插入#define NDEBUG 来禁用assert调用。

#include <stdio.h>
#define NDEBUG
#include <assert.h>

NDEBUG宏是Standard C中定义的宏,专门用来控制assert()的行为。如果定义了这个宏,则assert不会起作用。

#ifdef NDEBUG
#define assert(x) ((void)0)
#else
...

注意:

数组和指针的区别

数组:数组是用于储存多个相同类型数据的集合。

指针:指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。

区别:

  1. 赋值:同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
  2. 存储方式
    • 数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下标进行访问的,多维数组在内存中是按照一维数组存储的,只是在逻辑上是多维的。
    • 指针:指针本身就是一个变量,32位机器上,一个指针占4个字节;64位机器上,一个指针占8个字节
  3. 求sizeof
    • sizeof(数组名):数组所占存储空间的内存
    • sizeof(指针名):指针变量占据的字节数

指针和引用的区别

引用是一个已经存在的对象的别名,指针存放某个对象的地址。

指针是对象,引用不是对象,程序为指针变量分配内存区域,而不为引用分配内存区域。

定义引用时必须初始化,之后无法改变。指针无须定义时赋初值,指针本身就是一个对象,允许指针赋值和拷贝,可以修改指向。因此没有空引用而有空指针,且指针总是应该被测试是否为空。

对引用使用“sizeof”得到的是所指向的变量(对象)的大小,对指针使用“sizeof”得到的是指针本身的大小。

引用是对象的别名,且引用不是对象,所以不存在引用的引用。可以有指针的指针。

++引用作用到引用所指的对象,++指针作用到指针自身,使指针指向下一个对象,而不是改变所指对象的内容。

C++为什么有了指针还要引入引用?

引用的底层也是指针实现的,引用和指针在编译结束之后汇编指令是一样的,引用会占用和指针相同大小的数据空间。

引用传递,只是明面上,没有使用值传递,值传递本身是不可避免的。编译器,暗地里通过指针(或者其他可以替代指针的数据类型)的值传递,替换了引用传递。

引用能实现的基本上指针都可以实现,那为什么C++还需要引入引用呢?

直接原因:是为了支持运算符重载。

如果没有引用,则得用指针来operator overloading操作。

A operator +(const A *a, const A *_a);  

那么使用的时候,&a + &b,这样看起来是不是很难受。而引入引用的概念,既可以满足overload operator,也不失重载value和pointer的灵活性。而且引用还带来一个指针无法替代的特性: 引用临时对象。因为引用必须在定义的时候就赋值,以后无法更改。

引用带来的好处:
用指针的使用经常犯得错:

  1. 操作空指针
  2. 操作野指针
  3. 误操作改变了指针的值,而后还以为该指针正常。

如果我们要正确的使用指针,我们不得不人为地保证这三个条件。而引用的提出就解决了这个问题。

引用区别于指针的特性是:

  1. 不存在空引用(保证不操作空指针)
  2. 必须初始化(保证不是野指针)
  3. 一个引用永远指向他初始化的那个对象,一旦初始化就不能改变初始化的值(保证指针值不变)。

人为保证变为了编译器来保证,更加安全

注意事项:

用户自定义的类型最好用引用传参,这样可以避免不必要的构造函数和析构函数调用,但是对于像int,long,char一类的内置类型,按值传参会比按引用传参更高效。

既然引入了引用,为何不抛弃指针呢?

答:为了兼容C语言,必然要支持指针

sizeof和strlen的区别

sizeof 运算符:查询对象或类型的大小,在必须知道对象实际大小时使用。

strlen函数:位于头文件<string.h>,查询空终止字节字符串 str 的长度。

语法

  1. sizeof 运算符

    //返回 size_t 类型值
    sizeof(类型);
    sizeof 表达式;
    1. 返回 类型 的对象表示的字节大小
    2. 返回 表达式 类型的对象表示的字节大小,不应用隐式转换到表达式

    不能用于函数类型、不完整类型(含void)或位域左值

  2. strlen函数

    size_t strlen(const char *str);

    返回空终止字节字符串 str 的长度。

区别

  1. strlen计算的是字符串的字符个数
  2. sizeof计算的是分配空间的字节数
  3. strlen是函数,在程序运行时计算,要求字符串以空字符(‘\0’)结尾
  4. sizeof是关键字,在程序编译时计算,不能用于返回动态分配的内存空间大小(没想到测试用例,可能意思是用sizeof得不到想要的结果,但是语法上是可以的)

32位机器上,一个指针占4个字节;64位机器上,一个指针占8个字节

d#include <iostream>

using std::cin;
using std::cout;
using std::endl;

void t1() {
cout << "hello";
}

int t2() {
return 1;
}

#pragma pack(push) //保存对齐状态
#pragma pack(4)//设定为4字节对齐
class t3 {
char c;
int a;
double b;
};

void t4(int a[10]) {
cout << sizeof(a) << endl;
}

void t5(char a[10]) {
cout << sizeof(a) << endl;
}

int main(int argc, char* argv[]) {
int a[10] = { 0 };
int* b = new int[10]{ 0 };
char c[] = "hello,world";
char d[3] = { 'a', 'b', 'c' };
t3* i = new t3[10]{ 0, 0 };

cout << sizeof(a) << endl; //输出:40 数组所占字节数
cout << sizeof(b) << endl; //输出:4 指针所占字节数
cout << strlen(c) << endl; //输出:11 第一个'\0'字符前面的字符个数
cout << sizeof(c) << endl; //输出:12 字符串(包括'\0'字符)所占字节数
cout << strlen(d) << endl; //输出:23 结果随机,无法预料,无意义
cout << sizeof(d) << endl; //输出:3 数组所占字节数
cout << sizeof(int) << endl;//输出:4 int类型的对象所占的字节数
//cout << sizeof(t1) << endl; 无法将函数作为参数,无论返回值是void还是int之类的
//cout << sizeof(t2) << endl;
cout << sizeof(*i) << endl; //输出:16 对象所占的字节数
//数组传递给函数作参数时,已经隐式转换成指针了
t4(a); //输出:4 指针所占字节数
t4(b); //输出:4 指针所占字节数

t5(c); //输出:4 指针所占字节数
t5(d); //输出:4 指针所占字节数
delete[] i;
return 0;
}

进阶

空类型的实例仍会占据内存空间,应为声明空类型实例时,必须在内存占有一定空间,否则无法使用这些实例。占多少内存由编译器决定。

Visual Studio中,每个空类型的的实例占用1字节的空间。

#include <iostream>

using std::cin;
using std::cout;
using std::endl;

class Y1 {

};

class Y2 {
public:
Y2() = default;
~Y2() = default;
};

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

int main(int argc, char* argv[]) {
Y1 a;
Y2 b;
Y3 c;
cout << sizeof(a) << endl; //1
cout << sizeof(b) << endl; //1
cout << sizeof(c) << endl; //4
return 0;
}

C++的编译器一旦发现一个类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每个实例中添加一个指向虚函数表的指针。

#pragma pack(n)

设定结构体、联合以及类成员变量以 n 字节方式对齐

#pragma pack(push)  // 保存对齐状态
#pragma pack(4) // 设定为 4 字节对齐

struct test
{
char m1;
double m4;
int m3;
};

#pragma pack(pop) // 恢复对齐状态

位域

Bit mode: 2;    // mode 占 2 位

类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。

extern “C”

extern "C" 的作用是让 C++ 编译器将 extern "C" 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif

struct 和 typedef struct

【详见文章:typedef与struct结构体定义】

C语言中:

// c
typedef struct Student {
int age;
} S;

等价于

// c
struct Student {
int age;
};

typedef struct Student S;

等价于

// c
typedef struct Student {
int age;
}S;

此时 S 等价于 struct Student,但两个标识符名称空间不相同。

另外还可以定义与 struct Student 不冲突的同名函数 void Student() {}

C++中:

由于编译器定位符号的规则(搜索规则)改变,导致不同于C语言。

如果在类标识符空间定义了 struct Student {...};,使用 Student me; 时,编译器将搜索全局标识符表,Student 未找到,则在类标识符内搜索,即表现为可以使用 Student 也可以使用 struct Student,如下:

// cpp
struct Student {
int age;
};

void f( Student me ); // 正确,"struct" 关键字可省略

若定义了与 Student 同名函数之后,则 Student 只代表函数,不代表结构体,如下:

typedef struct Student { 
int age;
} S;

void Student() {} // 正确,定义后 "Student" 只代表此函数

//void S() {} // 错误,符号 "S" 已经被定义为一个 "struct Student" 的别名

int main() {
Student();
struct Student me; // 或者 "S me";
return 0;
}

C++ 中 struct 和 class

总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。

区别:

union 联合

联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。

联合有如下特点:

#include<iostream>

union UnionTest {
UnionTest() : i(10) {};
int i;
double d;
};

static union {
int i;
double d;
};

int main() {
UnionTest u;

union {
int i;
double d;
};

std::cout << u.i << std::endl; // 输出 UnionTest 联合的 10

::i = 20;
std::cout << ::i << std::endl; // 输出全局静态匿名联合的 20

i = 30;
std::cout << i << std::endl; // 输出局部匿名联合的 30

return 0;
}

C 实现 C++ 类

C 实现 C++ 的面向对象特性(封装、继承、多态)

[Can you write object-oriented code in C? closed]

explicit(显式)关键字

【详见C++ Primer第7章笔记】

关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于隐式转换,所以无须声明explicit。explicit声明只能在类内声明,类外部定义时不应重复

发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=),此时只能使用直接初始化而不能使用explicit构造函数

尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显示地强制进行转换:

explicit Sales_data(const std::string &s): bookNo(s){}
explicit Sales_data(std::istream&){}

//正确:实参是一个显示构造的Sales_data对象
item.combine(Sales_data(null_book));
//正确:static_cast使用explicit的构造函数创建了一个临时的Sales_data对象
item.combine(static_cast<Sales_data>(cin));

explicit 使用:

struct A
{
A(int) { }
operator bool() const { return true; }
};

struct B
{
explicit B(int) {}
explicit operator bool() const { return true; }
};

void doA(A a) {}

void doB(B b) {}

int main()
{
A a1(1); // OK:直接初始化
A a2 = 1; // OK:复制初始化
A a3{ 1 }; // OK:直接列表初始化
A a4 = { 1 }; // OK:复制列表初始化
A a5 = (A)1; // OK:允许 static_cast 的显式转换
doA(1); // OK:允许从 int 到 A 的隐式转换
if (a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a6(a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a7 = a1; // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化

B b1(1); // OK:直接初始化
B b2 = 1; // 错误:被 explicit 修饰构造函数的对象不可以复制初始化
B b3{ 1 }; // OK:直接列表初始化
B b4 = { 1 }; // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化
B b5 = (B)1; // OK:允许 static_cast 的显式转换
doB(1); // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换
if (b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
bool b6(b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
bool b7 = b1; // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换
bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化

return 0;
}

friend 友元类和友元函数

using

【详见文章:namespace命名空间】

using 声明

一条 using 声明 语句一次只引入命名空间的一个成员。它使得我们可以清楚知道程序中所引用的到底是哪个名字。如:

using namespace_name::name;

引入声明可隐藏另一个同名的引入声明,但如果本身存在一个声明与引入声明同名,则出现重复声明错误。

构造函数的 using 声明

在 C++11 中,派生类能够重用其直接基类定义的构造函数。

可重复声明,但必须明确使用。

class Derived : Base {
public:
using Base::Base;
/* ... */
};

如上 using 声明,对于基类的每个构造函数,编译器都生成一个与之对应(形参列表完全相同)的派生类构造函数。生成如下类型构造函数:

Derived(parms) : Base(args) { }

using 指令

using 指令 使得某个特定命名空间中所有名字都可见,这样我们就无需再为它们添加任何前缀限定符了。如:

using namespace_name name;

尽量少使用 using 指令 污染命名空间

一般说来,使用 using 命令比使用 using 编译命令更安全,这是由于它只导入了指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译命令导入所有的名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。

using 使用

尽量少使用 using 指令

using namespace std;

应该多使用 using 声明

int x;
std::cin >> x ;
std::cout << x << std::endl;

或者

using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;

:: 范围解析运算符

分类:

  1. 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
  2. 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
  3. 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的

使用:

int count = 11;         // 全局(::)的 count

class A {
public:
static int count; // 类 A 的 count(A::count)
};
int A::count = 21;

namespace B {
int count = 12;
}

int main() {
::count = 12; // 测试 1:设置全局的 count 的值为 12

A::count = 22; // 测试 2:设置类 A 的 count 为 22

B::count = 32; // 测试 3:设置命名空间 B 的count 为 32

return 0;
}

enum 枚举类型

限定作用域的枚举类型

enum class open_modes { input, output, append };

不限定作用域的枚举类型

enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };

decltype

decltype 关键字用于检查实体的声明类型或表达式的类型及值分类。语法:

decltype ( expression )

decltype 使用

// 尾置返回允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
// 处理序列
return *beg; // 返回序列中一个元素的引用
}
// 为了使用模板参数成员,必须用 typename
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
// 处理序列
return *beg; // 返回序列中一个元素的拷贝
}

引用

左值引用

常规引用,一般表示对象的身份。

右值引用

右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。

右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:

引用折叠

宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对 “参数” 进行的是一对一的替换

成员初始化列表

好处

initializer_list 列表初始化

花括号初始化器列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数.

initializer_list 使用

#include <iostream>
#include <vector>
#include <initializer_list>

template <class T>
struct S {
std::vector<T> v;
S(std::initializer_list<T> l) : v(l) {
std::cout << "constructed with a " << l.size() << "-element list\n";
}
void append(std::initializer_list<T> l) {
v.insert(v.end(), l.begin(), l.end());
}
std::pair<const T*, std::size_t> c_arr() const {
return {&v[0], v.size()}; // 在 return 语句中复制列表初始化
// 这不使用 std::initializer_list
}
};

template <typename T>
void templated_fn(T) {}

int main()
{
S<int> s = {1, 2, 3, 4, 5}; // 复制初始化
s.append({6, 7, 8}); // 函数调用中的列表初始化

std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";

for (auto n : s.v)
std::cout << n << ' ';
std::cout << '\n';

std::cout << "Range-for over brace-init-list: \n";

for (int x : {-1, -2, -3}) // auto 的规则令此带范围 for 工作
std::cout << x << ' ';
std::cout << '\n';

auto al = {10, 11, 12}; // auto 的特殊规则

std::cout << "The list bound to auto has size() = " << al.size() << '\n';

// templated_fn({1, 2, 3}); // 编译错误!“ {1, 2, 3} ”不是表达式,
// 它无类型,故 T 无法推导
templated_fn<std::initializer_list<int>>({1, 2, 3}); // OK
templated_fn<std::vector<int>>({1, 2, 3}); // 也 OK
}

面向对象

面向对象程序设计(Object-oriented programming,OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。

面向对象三大特征 —— 封装、继承、多态

面向对象特征

封装

把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

关键字:public, protected, private。默认为 private。

继承

多态

The Four Polymorphisms in C++

静态多态(编译期/早绑定)

函数重载

class A
{
public:
void do(int a);
void do(int a, int b);
};

动态多态(运行期期/晚绑定)

注意:

动态多态使用

class Shape                     // 形状类
{
public:
virtual double calcArea()
{
...
}
virtual ~Shape();
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
class Rect : public Shape // 矩形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
Shape * shape2 = new Rect(5.0, 6.0);
shape1->calcArea(); // 调用圆形类里面的方法
shape2->calcArea(); // 调用矩形类里面的方法
delete shape1;
shape1 = nullptr;
delete shape2;
shape2 = nullptr;
return 0;
}

虚析构函数

虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。

虚析构函数使用

class Shape
{
public:
Shape(); // 构造函数不能是虚函数
virtual double calcArea();
virtual ~Shape(); // 虚析构函数
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
shape1->calcArea();
delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
shape1 = NULL;
return 0
}

纯虚函数

纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。

virtual int A() = 0;

虚函数、纯虚函数

CSDN . C++ 中的虚函数、纯虚函数区别和联系

虚函数指针、虚函数表

C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现

虚继承

虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。

底层实现原理与编译器相关,一般通过虚基类指针虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

虚继承、虚函数

模板类、成员模板、虚函数

抽象类、接口类、聚合类

内存分配和管理

malloc、calloc、realloc、alloca

  1. malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
  2. calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。
  3. realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
  4. alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。

malloc、free

用于分配、释放内存

malloc、free 使用

申请内存,确认是否申请成功

char *str = (char*) malloc(100);
assert(str != nullptr);

释放内存后指针置空

free(p); 
p = nullptr;

new、delete

  1. new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)。
  2. delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。
  3. new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。

new、delete 使用

申请内存,确认new是否申请成功无法通过判空判断

int main()
{
T* t = new T(); // 先内存分配 ,再构造函数
delete t; // 先析构函数,再内存释放
return 0;
}

placement new

定位 new(placement new)允许我们向 new 传递额外的地址参数,从而在预先指定的内存区域创建对象。

new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }

new与malloc的区别,delete和free的区别

  1. new和delete是关键字,需要编译器的支持;malloc和free是库函数,需要包含库头文件。
  2. malloc/free是c/c++标准库的函数,只负责动态地申请和释放内存。new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现),然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
  3. malloc/free需要手动计算类型大小且返回值是void*,new/delete可以自己计算类型大小,返回对应的指针。

如果问到malloc,还有可能问你memcpy等realloc函数能不能在C++中使用,绝对不能,因为这些函数进行的都是内存值拷贝(也就是对象的浅拷贝),会发生浅拷贝这个严重的问题!

delete this 合法吗?

Is it legal (and moral) for a member function to say delete this?

合法,但:

  1. 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的
  2. 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数
  3. 必须保证成员函数的 delete this 后面没有调用 this 了
  4. 必须保证 delete this 后没有人使用了

如何定义一个只能在堆上(栈上)生成对象的类?

如何定义一个只能在堆上(栈上)生成对象的类?

只能在堆上

方法:将析构函数设置为私有

原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

只能在栈上

方法:将 new 和 delete 重载为私有

原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。

智能指针

C++ 标准库(STL)中

头文件:#include <memory>

C++ 98

std::auto_ptr<std::string> ps (new std::string(str));

C++ 11(实际意义上的智能指针)

  1. shared_ptr
  2. unique_ptr
  3. weak_ptr
  4. auto_ptr(被 C++11 弃用)

shared_ptr

多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。

weak_ptr

weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。

unique_ptr

unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有者被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。

auto_ptr

被 c++11 弃用,原因是缺乏语言特性如 “针对构造和赋值” 的 std::move 语义,以及其他瑕疵。

auto_ptr 与 unique_ptr 比较

强制类型转换运算符

显示转换

MSDN . 强制转换运算符

static_cast

向上转换是一种隐式转换。

dynamic_cast

const_cast

reinterpret_cast

bad_cast

bad_cast 使用

try {  
Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);
}
catch (bad_cast b) {
cout << "Caught: " << b.what();
}

运行时类型信息 (RTTI)

dynamic_cast

typeid

type_info

typeid、type_info 使用

#include <iostream>
using namespace std;

class Flyable // 能飞的
{
public:
virtual void takeoff() = 0; // 起飞
virtual void land() = 0; // 降落
};
class Bird : public Flyable // 鸟
{
public:
void foraging() {...} // 觅食
virtual void takeoff() {...}
virtual void land() {...}
virtual ~Bird(){}
};
class Plane : public Flyable // 飞机
{
public:
void carry() {...} // 运输
virtual void takeoff() {...}
virtual void land() {...}
};

class type_info
{
public:
const char* name() const;
bool operator == (const type_info & rhs) const;
bool operator != (const type_info & rhs) const;
int before(const type_info & rhs) const;
virtual ~type_info();
private:
...
};

void doSomething(Flyable *obj) // 做些事情
{
obj->takeoff();

cout << typeid(*obj).name() << endl; // 输出传入对象类型("class Bird" or "class Plane")

if(typeid(*obj) == typeid(Bird)) // 判断对象类型
{
Bird *bird = dynamic_cast<Bird *>(obj); // 对象转化
bird->foraging();
}

obj->land();
}

int main(){
Bird *b = new Bird();
doSomething(b);
delete b;
b = nullptr;
return 0;
}

Effective C++

  1. 视 C++ 为一个语言联邦(C、Object-Oriented C++、Template C++、STL)
  2. 宁可以编译器替换预处理器(尽量以 constenuminline 替换 #define
  3. 尽可能使用 const
  4. 确定对象被使用前已先被初始化(构造时赋值(copy 构造函数)比 default 构造后赋值(copy assignment)效率高)
  5. 了解 C++ 默默编写并调用哪些函数(编译器暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符、析构函数)
  6. 若不想使用编译器自动生成的函数,就应该明确拒绝(将不想使用的成员函数声明为 private,并且不予实现)
  7. 为多态基类声明 virtual 析构函数(如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数)
  8. 别让异常逃离析构函数(析构函数应该吞下不传播异常,或者结束程序,而不是吐出异常;如果要处理异常应该在非析构的普通函数处理)
  9. 绝不在构造和析构过程中调用 virtual 函数(因为这类调用从不下降至 derived class)
  10. operator= 返回一个 reference to *this (用于连锁赋值)
  11. operator= 中处理 “自我赋值”
  12. 赋值对象时应确保复制 “对象内的所有成员变量” 及 “所有 base class 成分”(调用基类复制构造函数)
  13. 以对象管理资源(资源在构造函数获得,在析构函数释放,建议使用智能指针,资源取得时机便是初始化时机(Resource Acquisition Is Initialization,RAII))
  14. 在资源管理类中小心 copying 行为(普遍的 RAII class copying 行为是:抑制 copying、引用计数、深度拷贝、转移底部资源拥有权(类似 auto_ptr))
  15. 在资源管理类中提供对原始资源(raw resources)的访问(对原始资源的访问可能经过显式转换或隐式转换,一般而言显示转换比较安全,隐式转换对客户比较方便)
  16. 成对使用 new 和 delete 时要采取相同形式(new 中使用 []delete []new 中不使用 []delete
  17. 以独立语句将 newed 对象存储于(置入)智能指针(如果不这样做,可能会因为编译器优化,导致难以察觉的资源泄漏)
  18. 让接口容易被正确使用,不易被误用(促进正常使用的办法:接口的一致性、内置类型的行为兼容;阻止误用的办法:建立新类型,限制类型上的操作,约束对象值、消除客户的资源管理责任)
  19. 设计 class 犹如设计 type,需要考虑对象创建、销毁、初始化、赋值、值传递、合法值、继承关系、转换、一般化等等。
  20. 宁以 pass-by-reference-to-const 替换 pass-by-value (前者通常更高效、避免切割问题(slicing problem),但不适用于内置类型、STL迭代器、函数对象)
  21. 必须返回对象时,别妄想返回其 reference(绝不返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。)
  22. 将成员变量声明为 private(为了封装、一致性、对其读写精确控制等)
  23. 宁以 non-member、non-friend 替换 member 函数(可增加封装性、包裹弹性(packaging flexibility)、机能扩充性)
  24. 若所有参数(包括被this指针所指的那个隐喻参数)皆须要类型转换,请为此采用 non-member 函数
  25. 考虑写一个不抛异常的 swap 函数
  26. 尽可能延后变量定义式的出现时间(可增加程序清晰度并改善程序效率)
  27. 尽量少做转型动作(旧式:(T)expressionT(expression);新式:const_cast<T>(expression)dynamic_cast<T>(expression)reinterpret_cast<T>(expression)static_cast<T>(expression)、;尽量避免转型、注重效率避免 dynamic_casts、尽量设计成无需转型、可把转型封装成函数、宁可用新式转型)
  28. 避免使用 handles(包括 引用、指针、迭代器)指向对象内部(以增加封装性、使 const 成员函数的行为更像 const、降低 “虚吊号码牌”(dangling handles,如悬空指针等)的可能性)
  29. 为 “异常安全” 而努力是值得的(异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏,分为三种可能的保证:基本型、强列型、不抛异常型)
  30. 透彻了解 inlining 的里里外外(inlining 在大多数 C++ 程序中是编译期的行为;inline 函数是否真正 inline,取决于编译器;大部分编译器拒绝太过复杂(如带有循环或递归)的函数 inlining,而所有对 virtual 函数的调用(除非是最平淡无奇的)也都会使 inlining 落空;inline 造成的代码膨胀可能带来效率损失;inline 函数无法随着程序库的升级而升级)
  31. 将文件间的编译依存关系降至最低(如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects;如果能够,尽量以 class 声明式替换 class 定义式;为声明式和定义式提供不同的头文件)
  32. 确定你的 public 继承塑模出 is-a(是一种)关系(适用于 base classes 身上的每一件事情一定适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象)
  33. 避免遮掩继承而来的名字(可使用 using 声明式或转交函数(forwarding functions)来让被遮掩的名字再见天日)
  34. 区分接口继承和实现继承(在 public 继承之下,derived classes 总是继承 base class 的接口;pure virtual 函数只具体指定接口继承;非纯 impure virtual 函数具体指定接口继承及缺省实现继承;non-virtual 函数具体指定接口继承以及强制性实现继承)
  35. 考虑 virtual 函数以外的其他选择(如 Template Method 设计模式的 non-virtual interface(NVI)手法,将 virtual 函数替换为 “函数指针成员变量”,以 tr1::function 成员变量替换 virtual 函数,将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数)
  36. 绝不重新定义继承而来的 non-virtual 函数
  37. 绝不重新定义继承而来的缺省参数值,因为缺省参数值是静态绑定(statically bound),而 virtual 函数却是动态绑定(dynamically bound)
  38. 通过复合塑模 has-a(有一个)或 “根据某物实现出”(在应用域(application domain),复合意味 has-a(有一个);在实现域(implementation domain),复合意味着 is-implemented-in-terms-of(根据某物实现出))
  39. 明智而审慎地使用 private 继承(private 继承意味着 is-implemented-in-terms-of(根据某物实现出),尽可能使用复合,当 derived class 需要访问 protected base class 的成员,或需要重新定义继承而来的时候 virtual 函数,或需要 empty base 最优化时,才使用 private 继承)
  40. 明智而审慎地使用多重继承(多继承比单一继承复杂,可能导致新的歧义性,以及对 virtual 继承的需要,但确有正当用途,如 “public 继承某个 interface class” 和 “private 继承某个协助实现的 class”;virtual 继承可解决多继承下菱形继承的二义性问题,但会增加大小、速度、初始化及赋值的复杂度等等成本)
  41. 了解隐式接口和编译期多态(class 和 templates 都支持接口(interfaces)和多态(polymorphism);class 的接口是以签名为中心的显式的(explicit),多态则是通过 virtual 函数发生于运行期;template 的接口是奠基于有效表达式的隐式的(implicit),多态则是通过 template 具现化和函数重载解析(function overloading resolution)发生于编译期)
  42. 了解 typename 的双重意义(声明 template 类型参数是,前缀关键字 class 和 typename 的意义完全相同;请使用关键字 typename 标识嵌套从属类型名称,但不得在基类列(base class lists)或成员初值列(member initialization list)内以它作为 base class 修饰符)
  43. 学习处理模板化基类内的名称(可在 derived class templates 内通过 this-> 指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符” 完成)
  44. 将与参数无关的代码抽离 templates(因类型模板参数(non-type template parameters)而造成代码膨胀往往可以通过函数参数或 class 成员变量替换 template 参数来消除;因类型参数(type parameters)而造成的代码膨胀往往可以通过让带有完全相同二进制表述(binary representations)的实现类型(instantiation types)共享实现码)
  45. 运用成员函数模板接受所有兼容类型(请使用成员函数模板(member function templates)生成 “可接受所有兼容类型” 的函数;声明 member templates 用于 “泛化 copy 构造” 或 “泛化 assignment 操作” 时还需要声明正常的 copy 构造函数和 copy assignment 操作符)
  46. 需要类型转换时请为模板定义非成员函数(当我们编写一个 class template,而它所提供之 “与此 template 相关的” 函数支持 “所有参数之隐式类型转换” 时,请将那些函数定义为 “class template 内部的 friend 函数”)
  47. 请使用 traits classes 表现类型信息(traits classes 通过 templates 和 “templates 特化” 使得 “类型相关信息” 在编译期可用,通过重载技术(overloading)实现在编译期对类型执行 if…else 测试)
  48. 认识 template 元编程(模板元编程(TMP,template metaprogramming)可将工作由运行期移往编译期,因此得以实现早期错误侦测和更高的执行效率;TMP 可被用来生成 “给予政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码)
  49. 了解 new-handler 的行为(set_new_handler 允许客户指定一个在内存分配无法获得满足时被调用的函数;nothrow new 是一个颇具局限的工具,因为它只适用于内存分配(operator new),后继的构造函数调用还是可能抛出异常)
  50. 了解 new 和 delete 的合理替换时机(为了检测运用错误、收集动态分配内存之使用统计信息、增加分配和归还速度、降低缺省内存管理器带来的空间额外开销、弥补缺省分配器中的非最佳齐位、将相关对象成簇集中、获得非传统的行为)
  51. 编写 new 和 delete 时需固守常规(operator new 应该内涵一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就应该调用 new-handler,它也应该有能力处理 0 bytes 申请,class 专属版本则还应该处理 “比正确大小更大的(错误)申请”;operator delete 应该在收到 null 指针时不做任何事,class 专属版本则还应该处理 “比正确大小更大的(错误)申请”)
  52. 写了 placement new 也要写 placement delete(当你写一个 placement operator new,请确定也写出了对应的 placement operator delete,否则可能会发生隐微而时断时续的内存泄漏;当你声明 placement new 和 placement delete,请确定不要无意识(非故意)地遮掩了它们地正常版本)
  53. 不要轻忽编译器的警告
  54. 让自己熟悉包括 TR1 在内的标准程序库(TR1,C++ Technical Report 1,C++11 标准的草稿文件)
  55. 让自己熟悉 Boost(准标准库)

More Effective C++

  1. 仔细区别 pointers 和 references(当你知道你需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由 pointers 达成,你就应该选择 references;任何其他时候,请采用 pointers)
  2. 最好使用 C++ 转型操作符(static_castconst_castdynamic_castreinterpret_cast
  3. 绝不要以多态(polymorphically)方式处理数组(多态(polymorphism)和指针算术不能混用;数组对象几乎总是会涉及指针的算术运算,所以数组和多态不要混用)
  4. 非必要不提供 default constructor(避免对象中的字段被无意义地初始化)
  5. 对定制的 “类型转换函数” 保持警觉(单自变量 constructors 可通过简易法(explicit 关键字)或代理类(proxy classes)来避免编译器误用;隐式类型转换操作符可改为显式的 member function 来避免非预期行为)
  6. 区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式(前置式累加后取出,返回一个 reference;后置式取出后累加,返回一个 const 对象;处理用户定制类型时,应该尽可能使用前置式 increment;后置式的实现应以其前置式兄弟为基础)
  7. 千万不要重载 &&||, 操作符(&&|| 的重载会用 “函数调用语义” 取代 “骤死式语义”;, 的重载导致不能保证左侧表达式一定比右侧表达式更早被评估)
  8. 了解各种不同意义的 new 和 delete(new operatoroperator newplacement newoperator new[]delete operatoroperator deletedestructoroperator delete[]
  9. 利用 destructors 避免泄漏资源(在 destructors 释放资源可以避免异常时的资源泄漏)
  10. 在 constructors 内阻止资源泄漏(由于 C++ 只会析构已构造完成的对象,因此在构造函数可以使用 try…catch 或者 auto_ptr(以及与之相似的 classes) 处理异常时资源泄露问题)
  11. 禁止异常流出 destructors 之外(原因:一、避免 terminate 函数在 exception 传播过程的栈展开(stack-unwinding)机制种被调用;二、协助确保 destructors 完成其应该完成的所有事情)
  12. 了解 “抛出一个 exception” 与 “传递一个参数” 或 “调用一个虚函数” 之间的差异(第一,exception objects 总是会被复制(by pointer 除外),如果以 by value 方式捕捉甚至被复制两次,而传递给函数参数的对象则不一定得复制;第二,“被抛出成为 exceptions” 的对象,其被允许的类型转换动作比 “被传递到函数去” 的对象少;第三,catch 子句以其 “出现于源代码的顺序” 被编译器检验对比,其中第一个匹配成功者便执行,而调用一个虚函数,被选中执行的是那个 “与对象类型最佳吻合” 的函数)
  13. 以 by reference 方式捕获 exceptions(可避免对象删除问题、exception objects 的切割问题,可保留捕捉标准 exceptions 的能力,可约束 exception object 需要复制的次数)
  14. 明智运用 exception specifications(exception specifications 对 “函数希望抛出什么样的 exceptions” 提供了卓越的说明;也有一些缺点,包括编译器只对它们做局部性检验而很容易不经意地违反,与可能会妨碍更上层的 exception 处理函数处理未预期的 exceptions)
  15. 了解异常处理的成本(粗略估计,如果使用 try 语句块,代码大约整体膨胀 5%-10%,执行速度亦大约下降这个数;因此请将你对 try 语句块和 exception specifications 的使用限制于非用不可的地点,并且在真正异常的情况下才抛出 exceptions)
  16. 谨记 80-20 法则(软件的整体性能几乎总是由其构成要素(代码)的一小部分决定的,可使用程序分析器(program profiler)识别出消耗资源的代码)
  17. 考虑使用 lazy evaluation(缓式评估)(可应用于:Reference Counting(引用计数)来避免非必要的对象复制、区分 operator[] 的读和写动作来做不同的事情、Lazy Fetching(缓式取出)来避免非必要的数据库读取动作、Lazy Expression Evaluation(表达式缓评估)来避免非必要的数值计算动作)
  18. 分期摊还预期的计算成本(当你必须支持某些运算而其结构几乎总是被需要,或其结果常常被多次需要的时候,over-eager evaluation(超急评估)可以改善程序效率)

Google C++ Style Guide

其他

数据结构

【详见文章:数据结构】

算法(Algorithm)

递归

概念:函数直接或间接地调用自身

递归与分治

递归与迭代

排序

查找

图搜索

图搜索算法 数据结构 遍历时间复杂度 空间复杂度
BFS广度优先搜索 邻接矩阵
邻接链表
O(|v|2)
O(|v|+|E|)
O(|v|2)
O(|v|+|E|)
DFS深度优先搜索 邻接矩阵
邻接链表
O(|v|2)
O(|v|+|E|)
O(|v|2)
O(|v|+|E|)

算法策略

算法 思想 应用
分治法 把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并 循环赛日程安排问题、排序算法(快速排序、归并排序)
动态规划 通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,适用于有重叠子问题和最优子结构性质的问题 背包问题、斐波那契数列
贪心法 一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法 旅行推销员问题(最短路径问题)、最小生成树、哈夫曼编码

操作系统

计算机网络

Unix网络编程

数据库

本节部分知识点来自《数据库系统概论(第 5 版)》

基本概念

常用数据模型

常用 SQL 操作

对象类型 对象 操作类型
数据库模式 模式 CREATE SCHEMA
基本表 CREATE SCHEMAALTER TABLE
视图 CREATE VIEW
索引 CREATE INDEX
数据 基本表和视图 SELECTINSERTUPDATEDELETEREFERENCESALL PRIVILEGES
属性列 SELECTINSERTUPDATEREFERENCESALL PRIVILEGES

SQL 语法教程:runoob . SQL 教程

关系型数据库

索引

数据库完整性

关系数据理论

范式

数据库恢复

并发控制

设计模式

设计模式的六大原则

链接装载库

本节部分知识点来自《程序员的自我修养——链接装载库》

内存、栈、堆

一般应用程序内存空间有如下区域:

栈保存了一个函数调用所需要的维护信息,常被称为堆栈帧(Stack Frame)或活动记录(Activate Record),一般包含以下几方面:

堆分配算法:

“段错误(segment fault)” 或 “非法操作,该内存地址不能 read/write”

典型的非法指针解引用造成的错误。当指针指向一个不允许读写的内存地址,而程序却试图利用指针来读或写该地址时,会出现这个错误。

普遍原因:

编译链接

各平台文件格式

平台 可执行文件 目标文件 动态库/共享对象 静态库
Windows exe obj dll lib
Unix/Linux ELF、out o so a
Mac Mach-O o dylib、tbd、framework a、framework

编译链接过程

  1. 预编译(预编译器处理如 #include#define 等预编译指令,生成 .i.ii 文件)
  2. 编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成 .s 文件)
  3. 汇编(汇编器把汇编码翻译成机器码,生成 .o 文件)
  4. 链接(连接器进行地址和空间分配、符号决议、重定位,生成 .out 文件)

现在版本 GCC 把预编译和编译合成一步,预编译编译程序 cc1、汇编器 as、连接器 ld

MSVC 编译环境,编译器 cl、连接器 link、可执行文件查看器 dumpbin

目标文件

编译器编译源代码后生成的文件叫做目标文件。目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。

可执行文件(Windows 的 .exe 和 Linux 的 ELF)、动态链接库(Windows 的 .dll 和 Linux 的 .so)、静态链接库(Windows 的 .lib 和 Linux 的 .a)都是按照可执行文件格式存储(Windows 按照 PE-COFF,Linux 按照 ELF)

目标文件格式

PE 和 ELF 都是 COFF(Common File Format)的变种

目标文件存储结构
功能
File Header 文件头,描述整个文件的文件属性(包括文件是否可执行、是静态链接或动态连接及入口地址、目标硬件、目标操作系统等)
.text section 代码段,执行语句编译成的机器代码
.data section 数据段,已初始化的全局变量和局部静态变量
.bss section BSS 段(Block Started by Symbol),未初始化的全局变量和局部静态变量(因为默认值为 0,所以只是在此预留位置,不占空间)
.rodata section 只读数据段,存放只读数据,一般是程序里面的只读变量(如 const 修饰的变量)和字符串常量
.comment section 注释信息段,存放编译器版本信息
.note.GNU-stack section 堆栈提示段

其他段略

链接的接口————符号

在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。

如下符号表(Symbol Table):

Symbol(符号名) Symbol Value (地址)
main 0x100
Add 0x123

Linux 的共享库(Shared Library)

Linux 下的共享库就是普通的 ELF 共享对象。

共享库版本更新应该保证二进制接口 ABI(Application Binary Interface)的兼容

命名

libname.so.x.y.z

路径

大部分包括 Linux 在内的开源系统遵循 FHS(File Hierarchy Standard)的标准,这标准规定了系统文件如何存放,包括各个目录结构、组织和作用。

动态链接器会在 /lib/usr/lib 和由 /etc/ld.so.conf 配置文件指定的,目录中查找共享库

环境变量

so 共享库的编写

使用 CLion 编写共享库

创建一个名为 MySharedLib 的共享库

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MySharedLib)

set(CMAKE_CXX_STANDARD 11)

add_library(MySharedLib SHARED library.cpp library.h)

library.h

#ifndef MYSHAREDLIB_LIBRARY_H
#define MYSHAREDLIB_LIBRARY_H

// 打印 Hello World!
void hello();

// 使用可变模版参数求和
template <typename T>
T sum(T t)
{
return t;
}
template <typename T, typename ...Types>
T sum(T first, Types ... rest)
{
return first + sum<T>(rest...);
}

#endif

library.cpp

#include <iostream>
#include "library.h"

void hello() {
std::cout << "Hello, World!" << std::endl;
}

so 共享库的使用(被可执行项目调用)

使用 CLion 调用共享库

创建一个名为 TestSharedLib 的可执行项目

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(TestSharedLib)

# C++11 编译
set(CMAKE_CXX_STANDARD 11)

# 头文件路径
set(INC_DIR /home/xx/code/clion/MySharedLib)
# 库文件路径
set(LIB_DIR /home/xx/code/clion/MySharedLib/cmake-build-debug)

include_directories(${INC_DIR})
link_directories(${LIB_DIR})
link_libraries(MySharedLib)

add_executable(TestSharedLib main.cpp)

# 链接 MySharedLib 库
target_link_libraries(TestSharedLib MySharedLib)

main.cpp

#include <iostream>
#include "library.h"
using std::cout;
using std::endl;

int main() {

hello();
cout << "1 + 2 = " << sum(1,2) << endl;
cout << "1 + 2 + 3 = " << sum(1,2,3) << endl;

return 0;
}

执行结果

Hello, World!
1 + 2 = 3
1 + 2 + 3 = 6

Windows 应用程序入口函数

_tWinMain 与 _tmain 函数声明

Int WINAPI _tWinMain(
HINSTANCE hInstanceExe,
HINSTANCE,
PTSTR pszCmdLine,
int nCmdShow);

int _tmain(
int argc,
TCHAR *argv[],
TCHAR *envp[]);
应用程序类型 入口点函数 嵌入可执行文件的启动函数
处理ANSI字符(串)的GUI应用程序 _tWinMain(WinMain) WinMainCRTSartup
处理Unicode字符(串)的GUI应用程序 _tWinMain(wWinMain) wWinMainCRTSartup
处理ANSI字符(串)的CUI应用程序 _tmain(Main) mainCRTSartup
处理Unicode字符(串)的CUI应用程序 _tmain(wMain) wmainCRTSartup
动态链接库(Dynamic-Link Library) DllMain _DllMainCRTStartup

部分知识点来自《Windows 核心编程(第五版)》

用处

注意

加载 Windows 程序的搜索顺序

  1. 包含可执行文件的目录
  2. Windows 的系统目录,可以通过 GetSystemDirectory 得到
  3. 16 位的系统目录,即 Windows 目录中的 System 子目录
  4. Windows 目录,可以通过 GetWindowsDirectory 得到
  5. 进程的当前目录
  6. PATH 环境变量中所列出的目录

DLL 入口函数

DllMain 函数

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
// 第一次将一个DLL映射到进程地址空间时调用
// The DLL is being mapped into the process' address space.
break;
case DLL_THREAD_ATTACH:
// 当进程创建一个线程的时候,用于告诉DLL执行与线程相关的初始化(非主线程执行)
// A thread is bing created.
break;
case DLL_THREAD_DETACH:
// 系统调用 ExitThread 线程退出前,即将终止的线程通过告诉DLL执行与线程相关的清理
// A thread is exiting cleanly.
break;
case DLL_PROCESS_DETACH:
// 将一个DLL从进程的地址空间时调用
// The DLL is being unmapped from the process' address space.
break;
}
return (TRUE); // Used only for DLL_PROCESS_ATTACH
}

载入卸载库

LoadLibrary、LoadLibraryExA、LoadPackagedLibrary、FreeLibrary、FreeLibraryAndExitThread 函数声明

// 载入库
HMODULE WINAPI LoadLibrary(
_In_ LPCTSTR lpFileName
);
HMODULE LoadLibraryExA(
LPCSTR lpLibFileName,
HANDLE hFile,
DWORD dwFlags
);
// 若要在通用 Windows 平台(UWP)应用中加载 Win32 DLL,需要调用 LoadPackagedLibrary,而不是 LoadLibrary 或 LoadLibraryEx
HMODULE LoadPackagedLibrary(
LPCWSTR lpwLibFileName,
DWORD Reserved
);

// 卸载库
BOOL WINAPI FreeLibrary(
_In_ HMODULE hModule
);
// 卸载库和退出线程
VOID WINAPI FreeLibraryAndExitThread(
_In_ HMODULE hModule,
_In_ DWORD dwExitCode
);

显示地链接到导出符号

GetProcAddress 函数声明

FARPROC GetProcAddress(
HMODULE hInstDll,
PCSTR pszSymbolName // 只能接受 ANSI 字符串,不能是 Unicode
);

DumpBin.exe 查看 DLL 信息

VS 的开发人员命令提示符 使用 DumpBin.exe 可查看 DLL 库的导出段(导出的变量、函数、类名的符号)、相对虚拟地址(RVA,relative virtual address)。如:

DUMPBIN -exports D:\mydll.dll

LoadLibrary 与 FreeLibrary 流程图

LoadLibrary 与 FreeLibrary 流程图

LoadLibrary

WindowsLoadLibrary

FreeLibrary

WindowsFreeLibrary

DLL 库的编写(导出一个 DLL 模块)

DLL 库的编写(导出一个 DLL 模块) DLL 头文件

// MyLib.h

#ifdef MYLIBAPI

// MYLIBAPI 应该在全部 DLL 源文件的 include "Mylib.h" 之前被定义
// 全部函数/变量正在被导出

#else

// 这个头文件被一个exe源代码模块包含,意味着全部函数/变量被导入
#define MYLIBAPI extern "C" __declspec(dllimport)

#endif

// 这里定义任何的数据结构和符号

// 定义导出的变量(避免导出变量)
MYLIBAPI int g_nResult;

// 定义导出函数原型
MYLIBAPI int Add(int nLeft, int nRight);

DLL 源文件

// MyLibFile1.cpp

// 包含标准Windows和C运行时头文件
#include <windows.h>

// DLL源码文件导出的函数和变量
#define MYLIBAPI extern "C" __declspec(dllexport)

// 包含导出的数据结构、符号、函数、变量
#include "MyLib.h"

// 将此DLL源代码文件的代码放在此处
int g_nResult;

int Add(int nLeft, int nRight)
{
g_nResult = nLeft + nRight;
return g_nResult;
}

DLL 库的使用(运行时动态链接 DLL)

DLL 库的使用(运行时动态链接 DLL)

// A simple program that uses LoadLibrary and 
// GetProcAddress to access myPuts from Myputs.dll.

#include <windows.h>
#include <stdio.h>

typedef int (__cdecl *MYPROC)(LPWSTR);

int main( void )
{
HINSTANCE hinstLib;
MYPROC ProcAdd;
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;

// Get a handle to the DLL module.

hinstLib = LoadLibrary(TEXT("MyPuts.dll"));

// If the handle is valid, try to get the function address.

if (hinstLib != NULL)
{
ProcAdd = (MYPROC) GetProcAddress(hinstLib, "myPuts");

// If the function address is valid, call the function.

if (NULL != ProcAdd)
{
fRunTimeLinkSuccess = TRUE;
(ProcAdd) (L"Message sent to the DLL function\n");
}
// Free the DLL module.

fFreeResult = FreeLibrary(hinstLib);
}

// If unable to call the DLL function, use an alternative.
if (! fRunTimeLinkSuccess)
printf("Message printed from executable\n");

return 0;
}

运行库(Runtime Library)

典型程序运行步骤

  1. 操作系统创建进程,把控制权交给程序的入口(往往是运行库中的某个入口函数)
  2. 入口函数对运行库和程序运行环境进行初始化(包括堆、I/O、线程、全局变量构造等等)。
  3. 入口函数初始化后,调用 main 函数,正式开始执行程序主体部分。
  4. main 函数执行完毕后,返回到入口函数进行清理工作(包括全局变量析构、堆销毁、关闭I/O等),然后进行系统调用结束进程。

一个程序的 I/O 指代程序与外界的交互,包括文件、管程、网络、命令行、信号等。更广义地讲,I/O 指代操作系统理解为 “文件” 的事物。

glibc 入口

_start -> __libc_start_main -> exit -> _exit

其中 main(argc, argv, __environ) 函数在 __libc_start_main 里执行。

MSVC CRT 入口

int mainCRTStartup(void)

执行如下操作:

  1. 初始化和 OS 版本有关的全局变量。
  2. 初始化堆。
  3. 初始化 I/O。
  4. 获取命令行参数和环境变量。
  5. 初始化 C 库的一些数据。
  6. 调用 main 并记录返回值。
  7. 检查错误并将 main 的返回值返回。

C 语言运行库(CRT)

大致包含如下功能:

C语言标准库(ANSI C)

包含:

书籍

【详见plan分类】

语言

算法

系统

网络

其他

C/C++发展方向

C/C++ 发展方向甚广,包括不限于以下方向, 以下列举一些大厂校招岗位要求。

后台/服务器

【后台开发】

桌面客户端

【PC 客户端开发】

图形学/游戏/VR/AR

【游戏客户端开发】

测试开发

【测试开发】

网络安全/逆向

【安全技术】

嵌入式/物联网

【嵌入式应用开发】

音视频/流媒体/SDK

【音视频编解码】

  1. 硕士及以上学历,计算机、信号处理、数学、信息类及相关专业和方向;
  2. 视频编解码基础扎实,熟常用的 HEVC 或 H264,有较好的数字信号处理基础;
  3. 掌握 C/C++,代码能力强, 熟悉一种汇编语言尤佳;
  4. 较强的英文文献阅读能力;
  5. 学习能力强,具有团队协作精神,有较强的抗压能力。

计算机视觉/机器学习

【计算机视觉研究】

<⇧>