C++ 概述

C++ 程序结构

  • C++ 是面向对象和过程的范式的集合

Comment

注释:被编译器忽略的文本

  • /**/:允许多行注释
  • //:单行注释

Library

库:提前编写的能执行有用操作工具集合

库引入

  • #include <>:头文件是C++标准中的系统库
  • #include ".h":其他库、自行编写的头文件

Namespace

命名空间:被切分为代码段的结构,为确保定义在大系统中各部分 程序的元素名称(如:变量名、函数名等)不会相互混淆

  • std:C++标准库的命名空间
  • 需要告诉编译器定义所属的命名空间才能引用头文件中的名称

函数

为方便理解,大多数程序被划分为几个较小的易于理解的函数

  • Prototype:函数原型,函数定义的首行加上分号结尾组成
  • 主程序:每个C++程序必须有main函数,指明程序计算 的开始点,main函数结束时,程序执行也随之结束

Variable

变量:一个命名的、能够存储特定类型值的一块内存区域

  • 在变量生存期内,所有变量的名字、类型都是不改变的, 而变量的值一般会随着程序运行发生改变

  • 可以将变量视为盒子

    • 变量名:作为和盒子的标签在盒子外
    • 变量值:在盒子里物品
  • C++/C中变量的解释是由变量类型而不是决定, 存储值一样,可能会有不同的处理方式

    • 引用、指针都存储地址,但是引用可以直接作为变量使用, 指针则需要解引用
  • initializer:初始化,初始值作为声明的一部分
  • 变量类型、修饰符参见cs_cppc/basics/mem_ctl

Scope

变量作用域

Local Variable

局部变量:声明在函数体内,作用域可以扩展到其 声明所在块

  • 函数被调用时:为每个局部变量分配在整个函数调用时期 的存储空间
  • 函数返回时:所有局部变量消亡
  • 局部变量一般存储在函数栈中

Global Variable

全局变量:声明在函数定义外,作用域为其声明所在文件

  • 生命期为程序运行的整个运行期,可用于存储函数调用的值
  • 可以被程序中任意函数操作,难以避免函数间相互干扰
  • 除声明全局常量外,不采用全局变量易于管理程序
  • 全局变量一般存储在静态区中

Declare

声明:主要功能是将变量的名字和变量包含值类型相关联

  • 在使用变量之前必须声明
  • 变量声明在程序中的位置决定了变量的scope
  • 事实上,函数、类等都可以提前声明,有些时候必须如此,以 使用forward reference(前向引用,定义之前声明指向其的 指针)

Identifier

标识符:变量、函数、类型、常量等名字的统称

  • 必须以字母、_开始
  • 所有字符必须是字母、数字、_,不允许空格或其他特殊字符
  • 不能包含保留字
  • 标识符中大小写字母是不同的
  • 标识符可以取任意长度,但是C++编译器不会考虑任何超过31个 字符的两个名字是否相同

Shadowing

遮蔽:程序代码内层块中变量隐藏外层块中同名变量的行为

隐式类型转换

数值类型转换

  • 提升型转换:通常不会造成数值差异

    • 小整数charshort转换到int
    • float转换到double
  • 可能存在转换误差的转换

    • 负数转换为无符号类型:二进制位不变,即为负数对应补码 正数
    • 其他类型转换为bool类型
    • 浮点数转换为整数:截断,若出现数值溢出,则出现未定义 行为

指针类型转换

  • 空指针void *、任意指针类型类型之间相互转换
  • 衍生类指针转换为基类指针,同时不改变constvolatile 属性
  • C风格数组隐式把数组转换为指向第一个元素的指针
    • 容易出现错误
      1
      2
      3
      char * s = "Help" + 3;
      # ”Help"被转换为指向数组指针,向后移3位
      # `s`指向最后元素`p`

强制类型转换

  • 上行转换:派生类指针、引用转换为基类表示
  • 下行转换:基类指针、引用转化为派生类表示

const_cast

const_cast:去掉原有类型的constvolatile属性,将常量 指针、引用转换为非常量

  • 常量指针转化为非常量指针,仍来指向原来对象
  • 常量引用转换为非常量引用,仍然指向原来对象
  • 一般用于修改指针,如const char *p
  • 要求期望去常量目标非常量,否则为未定义行为
1
2
3
4
5
6
7
8
9
10
11
int ary[4] = {1, 2, 3, 4};
// 去常量化目标非常量
const int * c_ptr = ary;
// 常量化数组指针
// 不能直接数组中值
int * ptr = const_cast<int*>(c_ptr);
// 去`const`,强制转换为非常量化指针
// 可以修过数组中值
for(int i = 0; i < 4; i++){
ptr[i] += 1;
}
未定义行例
  • 堆区常量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    int con_cast(){
    const int * c_val_ptr = new const int(1);
    // 常量值,非常量指针
    int vec_before[*c_val_ptr];
    // 常量声明数组
    int & ptr_val = const_cast<int &>(*c_val_ptr);
    ptr_val += 1;
    // 可以正常给值加1
    // 未定义行为?堆区不存在常量?
    int vec_after[*c_val_ptr];
    // 常量生命数组
    cout << sizeof(vec_before) << endl <<
    sizeof(vec_after) << after;
    // 二者长度均为`8`,即常量在`vec_before`创建前已处理
    cout << *c_val_ptr << endl << ptr_val << endl
    << c_val_ptr << endl << &ptr_val;
    // 地址、值均相同
    }
  • 栈区常量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int con_cast(){
    const int c_val= 1;
    // 常量值
    int vec_before[c_val];
    // 常量声明数组
    int & ptr_val = const_cast<int &>(&c_val);
    ptr_val += 1;
    // 可以正常给值加1
    int vec_after[c_val];
    // 常量生命数组
    cout << sizeof(vec_before) << endl <<
    sizeof(vec_after) << after;
    // 二者长度均为`4`,即常量值已处理,但没有改变
    cout << c_val << endl << ptr_val << endl
    << &c_val<< endl << &ptr_val;
    // 地址保持相同、但二者值不同
    }
  • 以上代码在g++4.8.5中测试

static_cast

static_cast:静态类型转换,无条件转换

  • 类层次中基类、派生类之间指针、引用转换

    • 上行转换:派生类完全包含基类所有成员,安全
    • 下行转换:派生类包含独有成员,没有动态类型检查,对象 为派生类实例时转换不安全
    • 基类、派生类之间转换建议使用dynamic_cast
  • 基本类型转换:安全性需要开发者维护

    • intcharenumfloat之间相互转换
    • 空指针转换为目标类型指针:不安全
    • 任何类型表达式转换为void类型
  • 不能进行无关类型指针(无继承关系、float与int等)之间 转换,而C风格强转可以
  • 不能转换掉原有类型的constvolatile__unaligned 属性
  • 静态是相对于动态而言,只在编译时检查,编译时已经确定转换 方式,没有运行时类型检查保证转换安全性
1
2
3
4
5
6
7
8
9
10
11
12
float f_pi = 3.141592f
int i_pi = static_cast<int>(f_pi)

Sub sub;
// 衍生类
Base * base_ptr = static_cast<Base*>(&sub);
// 上行转换,安全

Base base;
// 基类
sub * sub_ptr = static_cast<Sub*>(&base);
// 下行转换,不安全
  • 和C风格强转效果基本一致(使用范围较小),同样没有运行时 类型检查保证转换安全性,有安全隐患
  • C++中所有隐式转换都是使用static_cast实现

dynamic_cast

dynamic_cast:指针、引用动态类型转换,有条件转换

  • 安全的基类、派生类之间转换

    • 转型对象为指针:转型失败返回NULL
    • 转型对象为引用:转型失败抛出异常
  • 动用runtime type information进行类型安全检查,会有效率 损失

    • 依赖虚函数表将基类指针转换为子类指针???
    • 检查对象实例类型,保证转换是安全的,不会出现 子类指针指向父类对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Base{
public:
void print(){
cout << "i' base" << endl;
}

virtual void virtual_foo() {};
}

class Sub: public Base{
public:
void print(){
cout << "i'm sub" << endl;
}
virtual void virtual_foo();
}

int main(){
cout << "Sub -> Base" << endl;
Sub * sub = new Sub();
sub -> print();
// 打印:`i'm sub`
Base * sub2base = dynamic_cast<Base*>(sub);
if (sub2base != NULL)l{
sub2base->print();
// 打印:`i'm base`
}
cout << "sub2base val: " << sub2base << endl;

cout << "Base -> Sub" << endl;
Base * base = new Base();
base->print();
// 打印:`i'm base`
Sub * base2sub = dynamic<Sub*>(base);
if (base2sub != NULL){
base2sub -> print();
// 未打印
}
count << "base2sub val: " << base2sub << endl;

delete sub;
delete base;
return 0;
}
  • 涉及面向对象的多态性、程序运行时的状态,主要是用于虚类 类型上行转换
  • 同编译器的属性设置有关,所以不能完全使用C语言的强制转换 替代,常用、不可缺少

reinterpret_cast

reinterpret_cast:仅仅是重新解释给出对象的比特模型 ,没有对值进行二进制转换

  • 用于任意指针、引用之间的转换
  • 指针、足够大的整数(无符号)之间的转换
1
2
3
4
5
6
int * ptr = new int(233);
uint32_t ptr_addr = reinterpret_cast<uint32_t>(ptr);
count << "ptr addr: " << hex << ptr << endl
<< "ptr_add val: " << hex << ptr_addr << endl;
// 二者输出值相同
delete ptr;
  • 处理无关类型转换,通常为位运算提供较低层次重新解释
  • 难以保证移植性

C风格

1
2
3
4
5
6
7
8
9
10
Typename b = (Typename) a;

float b = 1.0f;
int c_i = (int)b;
int & c_j = (int&)b;
// C风格

int cpp_i = static_cast<int>(b);
int & j = reinterpret_cast<int&>(b);
// 等价C++风格
  • 没有运行时类型检查保证转换安全性,可能有安全隐患

Data Type

数据类型:从形式上看,数据类型有两个属性定义

  • domain:值集,该类型值的集合
  • set of operation:操作集,定义类型的行为
  • C++每个数据值都有其相应数据类型
  • Primitive Type:基本类型,类型系统整体的建筑块
    • 整型
    • 浮点型
    • 布尔型
    • 字符
    • 枚举类型

Integer

C++定义了3种整数类型:shortintlong

  • 由值域大小相互区别

值域

  • C++中没有指定3种类型确切值域,其取决于机器、编译器,但是 设计者可以更确切的定义各整形值域

    • shortintlong类型内存不减
    • int类型最大值至少$2^{15}-1$
    • long类型最大值至少$2^{31}-1$
  • 一般的

    • short:2bytes
    • int:4bytes
    • long/long long:8bytes
  • 若希望明确值域,尝试<cstdint>中自定义类型

unsigned

  • 各整形均可以在其类型之前加上关键字unsigned,构建新的 非负整形

  • 无符号整型可以提供有符号整型两倍正值域

  • 16进制、8进制等都是无符号输出格式,有符号整形会被 隐式转换位无符号(若传入)

表示

  • 整形常量一般写成十进制数字
  • 数字以0开始:编译器将其视为八进制数字
  • 数字以0x开始:编译器将其视为16进制数字
  • 数字L结尾:显式指明整数常量的类型为long(表达式中)
  • 数字U结尾:整形常数被认为时无符号整数(表达式中)

Float-Point

C++中定义了3种浮点类型:floatdoublelong double

值域

C++同样没由指定这些类型的确切表示

  • floatdoublelong double占用内存不减、精度 不减

表示

  • 通常使用带有小数点的十进制数字
  • 支持科学计数法风格:浮点数乘以十的整数幂

Bool

布尔类型:具有合法常量值truefalse的数据类型

Char

C++中表示字符的预定义基本类型:char

  • C++标准库定义wchar_t类型表示宽字符以扩展ASCII编码范围

值域

出现在屏幕、键盘上的字母、数字、空格、标点、回车等字符集合

  • 在机器内部,这些字符被表示成计算机赋给每个字符的数字代码
  • 多数C++实现中,表示字符的代码系统为ASCII

表示

  • '':单引号括起的一个字符表示字符常量
  • escape sequence:转移序列,以\开始的多个字符表示 特殊字符

对比整形

  • 整形没有1byte大小类型,很多情况下使用char类型存储 整数值以节省空间

  • 但C/C++某些[unsigned ]char、整形处理有区别

输入、输出
  • [unsigned ]char类型总是输出字符,而不是数字串

    • 输出流:关于整形的流操纵符对[unsigned ]char无效, 即使是无符号类型
    • 格式化输出:指定输出格式得到数字串,包含隐式类型转换

String

字符串:字符序列

  • C风格的字符串就是以\0结尾的字符数组
  • \0null character,空字符,对应ASCII码为0

表示

  • "":双引号括起的字符序列表示字符串常量
  • 允许使用转移序列表示字符串中特殊字符
  • 两个、两个以上字符串连续出现在程序中,编译器会自动将其 连接(即可以将字符串分行书写)

Enumerated

枚举类型:通过列举值域中元素定义的新的数据类型

1
enum typename { namelist };

值域

  • 默认的,编译器按照常量名顺序,从0开始给每个常量赋值
  • 允许给每个枚举类型常量显式的赋值
  • 若只给部分常量名赋值,则编译器自动给未赋值常量赋最后一个 常量值后继整数值

复合类型

基于已存在的类型创建的新类型

表达式

C++中表达式由项、操作符构成

  • term:项,代表单个数据的值,必须是常量、变量、函数调用
  • operator:操作符,代表计算操作的字符(短字符序列)
    • binary operator:二元操作符,要求两个操作数
    • unary operator:一元操作符,要求一个操作数
  • full expression:完整表达式,不是其他表达式子表达式 的表达式

表达式求值顺序

C++没有规定表达式求值顺序(表达式求值顺序是指CPU计算 表达式的顺序,不同于优先级、结合律)

  • sequenced before:若A按顺序先于B,A中任何计算都 先于B中任何计算
  • sequenced after:若A按顺序后于B,A中任何计算都晚于 B中任何计算
  • unsequenced:若A与B无顺序,则A、B中计算发生顺序 不确定,并且可能交叉
  • indeterminately sequenced:若A与B顺序不确定,则 A、B计算发生顺序不确定,但不能交叉
  • 对无顺序、顺序不确定求值,不要求两次不同求值使用相同顺序
  • 完整表达式的求值、副作用先于下个完整表达式的求值、 副作用

  • 表达式中不同子表达式的求值无顺序

    • 除非特殊说明,运算符的不同操作数求值是无顺序的
    • 运算操作数值计算先于运算符结果值计算
  • 对同一简单对象

    • 两个不同副作用无顺序,则为无定义行为
    • 副作用与需要此对象值的计算无顺序,则为无定义行为
    1
    2
    3
    int i = 0;
    i++ + i++; // 两`i++`对i`均副作用、无顺序,未定义行为
    i + i++; // `i++`副作用、`+`计算无顺序,未定义行为
  • 函数调用时:不仅限于显示函数调用,包括运算符重载、构造、 析构、类型转换函数

    • 实参求值、副作用先于函数体任何语句、表达式求值

    • 函数不同实参求值、副作用无顺序

      1
      2
      3
      int func(int, int);
      int i = 0;
      func(i++, i++); // 参数计算`i++`对`i`均有副作用,无定义行为
    • 主调函数中任何既不先于、也不后于被调函数的求值,其 与被调用函数都是顺序未指定,即主调函数中任何求值 与被掉函数不交叉

      1
      2
      3
      4
      int foo(int);
      int i = 0, j = 0, k = 0;
      (i++ + k) + foo(j++);
      (i++ + k) + foo(i++);
    • 不同形参初始化是顺序未指定

  • 自增、自减

    • 后缀形式:i++i--
      • 值计算先于对变量的修改
      • 与其顺序未指定函数调用不能插入值计算、变量修改 之间
    • 前缀形式:++i--i
      • 返回被更新之后的操作数(左值)
      • i非布尔值时,++i--i等价于i+=1i-=1
  • new

    • 内存分配函数与初始化参数求值顺序未指定
    • 新建对象初始化先于new表达式计算
  • 逻辑与&&、或||为短路求值

    • 左操作数计算先于右操作数计算
    • 左操作数为falsetrue时,右操作数不会被求值
  • ?:中三个操作数只有两个会被求值

    • 第一个操作数求值先于后两个操作数求值
    • 第一个操作数值为true时,第二个操作数被求值,否则 第三个操作数被求值
  • 赋值运算符=

    • 左、右操作数求值先于赋值操作
    • 赋值操作先于赋值表达式值计算
    • 赋值表达式返回其左操作数的左值,此时左操作数必然被 赋值
  • 复合赋值运算符e1 op= e2+=-=

    • 求值包括e1 op e2、结果赋给e1、返回e1
    • 任何函数调用不能插入以上步骤
  • 逗号,运算符(注意区分逗号分隔符)

    • 左操作数值被丢弃
    • 左操作数值计算、副作用先于右操作数值计算、副作用
    • 被重载后的逗号运算符将生成函数调用,对操作数求值遵循 函数实参求值顺序
  • 序列初始化:对{}存在的多个初始化参数求值

    • 初始化参数值计算、副作用先于被逗号分隔的后初始化值 计算、副作用
    • 即使初始化参数引起函数调用,列表每个值作为函数参数, 求值顺序仍然被保留

优先级、结合律

  • precedence:优先级,默认情况下(无括号)操作符在运算中 结合的方法

  • associativity:结合律,相同优先级的运算符运算顺序, 也即左右操作数的运算顺序

    • left-associative:左结合的,优先计算操作符左侧 表达式,大部分操作符时左结合的
    • right-assiciative:右结合的
优先级递减 结合性
()[]->.
一元操作符:-+--++!&*~(类型)sizeof
*/%
+-
>><<(右、左移位)
<<=>>=
==!=
&
^
` `
&&
` `
?:
=op=
  • 操作符只是语言的语法,其行为只是人为赋予的规则,其行为 可能符合逻辑,也可能不符合逻辑

混合类型

对于具有不同操作数的操作符,编译器会将操作数转化为其中精度 最高的类型,计算结果也为精度最高的类型,保证计算结果尽可能 精确

整数除法、求余

  • 两个整数除法运算:结果为整数,余数(小数)被舍去
  • 含负数操作数的除法、求余:依赖硬件特征
    • 求余一般返回同余正值

Type Cast

(值/静态)类型转换:将一种类型明确的转换为另一种类型

1
2
3
4
type(expr)
# C++风格转换
(type)expr
# C风格类型转换
  • 转换目标类型精度增加不丢失信息,否则可能会丢失信息

  • 符号整形转换无符号整形:依赖硬件特征,一般

    • 符号位置于最高位
    • 数值位根据精度
      • 同精度:不变,即包括符号位在内无改变
      • 低精度向高精度:高位用符号位补齐
      • 高精度向低精度:截断、保留低位
  • 无符号整形转符号整形

    • 同精度、高精度向低精度:截断、保留低位
    • 低精度向高精度:高位补0
  • 即:有符号转为其他类型(有符号、无符号),优先保留符号位

赋值操作

C++中,对变量的赋值是一种内置的表达式结构

  • 赋值操作符=要求其左操作数必须是可变的,通常是变量名

  • 首先计算赋值操作符右边表达式值,再赋给左边的变量

    • 右操作数可能需要进行类型转换以使其与左操作数的类型 相匹配
  • 赋值操作默认(未重载)是通过将源对象所有变量域(栈中 数据),复制到目标对象相应变量域实现的

返回值

C++赋值表达式返回右边表达式的值

  • 可以被组合进更大表达式中(但会影响阅读)
  • multiple assignment:多重赋值,可以方便给多个变量赋 相同值

Shorthand Assignment

将赋值操作符、二元操作符相结合产生形式

1
2
3
var op= expr;
var = var op expr;
// 等价

自增、自减

对变量进行+1-1更高级别的缩写形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x++;
// 后缀形式
// 自增前将其原始值返回给临近表达式
++x;
// 前缀形式
// 自增后将新值返回给临近表达式
x += 1;
x = x+1;
// 等价
y--;
--y;
y -= 1;
y = y-1;
// 等价

布尔运算

Relational Operator

关系操作符

  • ==:等于,容易犯错
  • !=:不等于
  • >:大于
  • <:小于
  • >=:大于等于
  • <=:小于等于

Logical Operator

逻辑操作符:采用布尔类型操作数,组合形成新的布尔值

  • !:逻辑非
  • &&:逻辑与
  • ||:逻辑或
Short-Circuit Evaluation

短路求值:得到结果时就立刻结束计算表达式

  • 依赖于:3种逻辑操作符优先级均不同,逻辑运算表达式总是 从左到右计算的

?:

三目操作符,需要3个操作数

1
2
3
(condition) ? expr_1 : expr_2
// `()`不必须,只是用于强调边界
// 首先计算`condition`,条件为`true`则返回`expr_1`

Bitwise Operator

位运算符:读取任意标量类型值,将其翻译成与底层硬件相应的 比特序列表示

  • &|^:位逻辑与、或、异或
  • ~:位逻辑非
  • >><<:右、左移位
    • 无符号数:字尾被移动比特数消失,另一端补0
    • 有符号数:行为依赖于硬件特征,一般保证乘除特性
      • 右移:补1
      • 左移:补0

语句

  • simple statement:简单语句,执行某些动作

    • 表达式加分号组成
  • control statement:控制语句,控制程序流程

    • 控制语句典型地应用在一条单一语句

Block

块:{}括起指明一组语句序列是连贯单元的一部分

  • 编译器会将整个块当作一条语句对待,也被称为 compound statement
  • 常用于使用特定控制语句控制一组语句

Conditional Execution

条件执行:根据检测条件控制程序后续执行

if

1
2
if (condition) statement
if (condition) statement else statement
  • if中控制语句可以是一条简单语句,也可以是一个语句块

switch

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (e){
// `e`:*control expression*
case c1:
//`c1`必须是常量标量
statements
break;
case c2:
statements
break;
default:
statements
break;
}
  • 程序计算控制表达式e的值,将结果同c1c2相比较

    • case后的常量必须是标量类型,即底层采用整数表示 的类型,如:整形、字符、枚举类型
  • 如果常量同控制表达式值相匹配,则跳转至相应case子句执行

    • 执行到子句中break时跳出switch语句
    • 若子句中无break,则接着执行之后case子句中语句, 直到遇到break/return跳出switch语句,这会带来很多 问题,除
      1
      2
      3
      4
      case 1:
      case 2:
      statement
      break;
  • default可选,执行没有和控制表达式匹配值的操作

    • 除非确定列举了所有可能情况,否则增加default子句是 好习惯

Iterative Statement

迭代语句:以循环的方式多次执行程序中的一部分

while

一般模式
1
2
3
while (condition-expression){
statements
}
  • 首先查看条件表达式值
  • 若条件表达式值为true,整个循环体被执行,然后返回到循环 开始检查条件表达式值
  • 若条件表达式值为false,则循环终止
  • 每个循环周期,包括第一次循环,条件表达式都会被测试,且 仅在循环开始进行,循环中间条件表达式值改变不会被注意
Read-util-Sentinel Pattern

读直到信号量模式:使用break语句在循环中结束最内层循环

1
2
3
4
5
while(true){
Prompt user and read in a value
if (value == sentinel) { break; }
Process the data value
}

for

以特定的循环次数重复执行某个操作

  • 基于条件的循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 一般模式
    for (init; test; step){
    statements
    }
    init;
    while(test){
    statements
    step;
    }
    // 二者等价

    // 常用模式
    for(int var=start; var <= finish; var++){
    // `var`:*index variable*
    statement
    }
    // 循环`finish - start`次
  • range-based for loop:基于范围的循环,C++11开始支持

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    for(type var: collection){
    statements
    }

    // `foreach.h`接口中的定义宏,提供类似功能
    foreach(type var in collection){
    }

    // C++编译器通过迭代器,将基于范围的循环转换为传统循环
    for(ctype::iterator it = collection.begin());
    it != collection.end(); it++){
    // `ctype`:集合类型
    }

编译、汇编、执行

步骤

Preprocess

预处理:生成.i预处理文件

  • 宏替换
  • 注释的消除
  • 寻找相关头文件/接口:除默认搜索路径,还可以通过环境变量
    设置
    • C_INCLUDE_PATH;C头文件搜索路径
    • CPLUS_INCLUDE_PATH:C++头文件搜索路径
    • CPATH:C/C++头文件搜索路径
1
2
$ g++ -E src.cpp > src.i
// 激活预处理,输出重定向到文件中

Compile

编译:将预处理后的文件编译为汇编语言,生成汇编文件.s

  • 编译单位为文件
1
$ g++ -S src.i -o src.s

Assemble

汇编:生成目标机器代码,二进制.o中间目标文件

  • .o通常仅解析了文件内部变量、函数,对于引用变量函数 还未解析,需要将其他目标文件引入
1
$ g++ -C src.s -o src.o
  • Windows下生成.obj文件

链接:链接目标代码,生成可执行程序

  • gcc通过调用ld进行链接
  • 主要是链接函数、全局变量
  • 链接器关注/链接二进制.o中间目标文件
  • Library File:若源文件太多,编译生成的中间目标文件 过多,把中间目标文件打包得到.lib/.a文件
1
$ g++ src.o -o a.out

执行

序列点

  • 对C/C++表达式,执行表达式有两个类型动作
    • 计算某个值
    • 产生副作用:访问volatile对象、原子同步、修改文件

C++内存控制

内存布局

程序内存结构

  • static area:静态区,存储程序指令(按位存储)、 全局变量

    • 位于地址编址号较小、接近地址空间开始处
    • 该区域中分配的内存大小在程序整个执行期间不发生改变
    • 正文段:CPU执行机器指令

      • 每个程序只有一个副本
      • 只读,避免程序因为意外事故而修改自身指令
    • 初始化数据段:程序中所有赋初值全局变量

    • 非初始化数据段(bss段):程序中所有未初始化全局变量

      • 内核将此段初始化为0
  • heap area:堆区,程序运行期间动态分配

    • 程序中未分配的可用内存池
    • 处于栈区、静态区之间
    • 缺乏组织
    • 需要寻址、操作速度慢
    • 可以用于存储编译时未知大小、可变数据
  • stack area:栈区,存放函数栈帧

    • 最高地址区
    • 程序每调用函数、方法都会在此内存区域中创建新的栈帧, 函数返回所创建栈帧会被撤销,释放内存
    • 操作迅速,不需要寻址
    • 数据大小已知、固定
  • 堆、栈以相反方向增长,方便任一区域都可以依照需要增长, 直到所有可用内存耗尽

内存分配

  • static allocation:静态分配,声明全局变量、常量时, 编译器为其在静态区中分配在整个程序生命周期内持久的内存 空间
  • automatic allocation:自动分配,调用函数时,编译器为 局部变量在栈帧分配存储空间,函数返回时空间自动释放
  • dynamical allocation:程序允许时,动态获得内存空间
  • stack frame:栈帧

    • 栈帧随机为函数中局部变量分配内存、地址
    • 栈帧中还包含额外信息,其结构取决于机器架构
  • 变量:C++中声明变量时,编译器必须保证给声明变量分配足够 内存存储该类型变量值,分配内存大小取决于变量类型

Pointer

  • C++设计原则:应该尽可能多的访问到有底层硬件提供的机制, 所以C++语言使得内存位置的地址对程序员可见

指针:值是内存中一个地址的数据项

  • 指针允许以压缩方式引用大的数据结构
  • 指针使得程序在运行时能够预订新的内存
  • 指针可以用于记录数据项之间关系

LvalueRvalue

  • lvalue:左值,引用内存中能够存储数据的内存单元的表达式
  • rvalue:右值,非左值表达式
  • xvalue:返回右值引用的函数、表达式
  • gvalue:lvalue、xvalue总称

  • 具体参见cs_program/program_design/language_design

左值引用

左值引用:只能绑定左值,绑定有其他对象内存空间的变量

  • 建立引用时是将内存空间绑定

    • 使用的是对象在内存中位置
    • 则被引用对象需要是左值
    • 则不能将右值绑定到左值引用上
  • 常量左值引用保证不能通过引用改变对应内存空间值

    • 尝试绑定右值引用时,编译器会自动为右值分配空间
    • 则可以将右值绑定在常量(左值)引用上
1
2
3
4
5
6
7
8
9
int foo(42);
int& bar = foo;
// OK:`foo`是左值,使用其在内存中位置
int& baz = 42;
// Err:`42`是右值,不能将其绑定在左值引用上
const int& quz = 42;
// OK:`42`是右值,但编译器可以为其开辟内存空间
int& garply = ++foo;
// OK:前置自增运算符返回左值

右值引用

右值引用&&只能且必须绑定右值

  • 考虑右值只能是字面常量、或临时对象,则右值引用

    • 是临时的、即将销毁
    • 不会在其他地方使用
  • 则接受、使用右值引用的代码,可以自由的接管所引用对象 的资源,无需担心对其他代码逻辑造成数据破坏

    • move sematics:与右值引用交换成员
1
2
3
4
5
6
7
8
9
int foo(42);
int&& baz = foo;
// Err:`foo`是左值,不能绑定在右值引用上
int&& quz = 42;
// OK:`42`是右值,可以绑定在右值引用上
int&& quux = foo * 1;
// OK:`foo * 1`结果是右值,可以绑定在右值引用上
int&& waldo = foo--;
// OK:后置自减运算符返回右值
Move Sematics
  • 使用左值引用对类型X赋值操作流程如下

    1
    2
    3
    4
    5
    X& X::operator=(X const & rhs){
    // make a clone of what rhs.m_pResource refers to
    // destruct the resource this.m_pResource refers to
    // attach the clone to this.m_pResource
    }
    • m_pResourceX拥有某种资源
  • 考虑如下代码中最后一行赋值的执行

    1
    2
    3
    X foo();
    X x;
    x = foo();
    • 克隆foo()返回的临时对象中资源
    • 析构x中资源,替换为foo()返回临时对象中资源副本
    • 析构foo()返回临时对象
  • 以上赋值流程效率低、没有必要,考虑交换xfoo()返回 临时对象资源副本,即move语义

    1
    2
    3
    X& x::operator=(X&& rhs){
    // swap this->m_pResource and rhs.m_pResource
    }
    • 执行同样x=foo()

      • 此赋值操作只有由编译器自动析构foo()返回的临时 对象
      • x.m_pResource被转移给临时对象,在临时对象析构 时被析构
    • 交换成员其实和右值引用没有必然联系

      • 在其他方法中同样可以交换成员对象
      • 但是只有在参为右值引用时,交换成员对象确保 不会对其他代码逻辑造成破坏
    • 右值引用参数函数是对左值引用参数函数的重载

      • 编译器优先为右值引用调用以右值引用为形参的函数
      • 区分右值引用、左值引用可以尽量节省资源
Perfect Forwarding

完美转发:

引用值类型

  • 无论左值引用、右值引用

    • 引用作为变量被保留,则其为左值
    • 否则为右值
  • 左值引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int foo(42);
    int & bar = foo;
    // `bar`是对`foo`的左值引用
    int & baz = bar;
    // `baz`是对`bar`的左值引用
    // `bar`左值引用本身是左值
    int qux = ++foo;
    // 前置自增运算符返回左值引用
    // 此时左值引用作为右值
  • 右值引用

    1
    2
    3
    4
    5
    6
    7
    class Type;
    void foo(Type&& bar){
    Type baz(bar);
    // `bar`是左值
    }
    Type&& qux();
    quxx = qux();

引用叠加

  • C++11中引用叠加规则如下

    • Type& & -> Type&
    • Type& && -> Type&
    • Type&& & -> Type&
    • Type&& && -> Type&&
  • C++11之前不支持引用叠加,以下代码报错

    1
    2
    typedef int& intR;
    typedef intR& intRR;

指针使用

声明指针

1
int *p1, *p2;
  • 编译器需要知道指针base type,才能正确的解释指针地址 中的数据

  • *用于指明变量为指针变量

    • 语法上属于变量名:声明时需要给每个指针变量标记*
    • 但拥有基类型:是用于声明、定义的类型
  • base type:基类型,指针所指对象的类型

指针使用

  • &:取地址
  • *dereferencing,解析引用,取指针所指向对象的值
  • ->:解析+选择操作符,取指针指向对象的成员

特殊指针

  • this指向当前对象

    • 解决二义性:引用当前对象的实例变量,即使其被形参、 局部变量覆盖
    • 有建议:总是使用this引用当前实例变量使代码更具有 可读性
    • 类方法调用都将this作为隐含参数,指向当前实例,即 主调函数中类实例
  • NULLnull pointer,空指针,不指向任何实际内存地址

    • 在内部表示为0
    • <cstddef>中已定义
    • 使用*解释空指针不合法,但不总是能检测出来

引用调用

C++内部通过指针实现引用调用

  • 参数通过引用传递时,栈帧会在调用时存储一个指针指向该值 的内存单元
    • 引用参数被声明为引用类型,编译器会自动解析其指针值
  • 可以通过明确调用指针替代引用调用的效果

Pointer Arithmetic

指针运算:对指针进行加减的运算

1
2
3
4
5
p + k
// **定义**为:`&array[k]`
*p++
// 一元运算符右结合,等价于`*(p++)`
// 检索数组当前元素,并将索引指向下个元素
  • 只有+-运算有意义,且有约束

    • 可以+-整数
    • 不能指针相加
    • 可以指针相减,返回两个指针之间数组元素数量
  • 建议使用数组索引而不是指针运算提高可靠性

Array

数组:较低级的多个数据值的集合

  • 特性
    • 有序
    • 同质
  • 约束
    • 数组分配内存大小固定
    • 数组大小不允许被获得
    • 不支持插入、删除元素
    • 不检查越界:重大安全隐患
  • C++提供的内置数组类型,基于从C语言继承而来的语言模型
  • 考虑到Vector集合类更加灵活方便,没有什么理由继续使用 数组

数组使用

1
2
3
4
5
6
7
8
type name[size];
// 声明大小为`size`、类型为`type`的数组`name`
name[idx];
// 选择数组`name`中`idx`处元素
type static_arr [] = {};
// 数组静态初始化
const int ARR_LEN = sizeof static_arr / sizeof static_arr[0];
// 获取数组长度
  • 声明:多数情况下,应该使用符号常量而不是确定的整数值指定 数组大小,以便修改代码

  • selection:通过数组名+[idx]选择元素

  • 静态初始化:可以忽略数组容量,编译器自动从初始化的元素 数目推断

  • 获取数组分配容量:基于数组同质性

数组容量

  • allocated size:声明时指定的数组容量
  • effective size:实际使用到的元素数目
  • 声明比需求大的数组
    • 定义常量表示数组元素数目最大值,以此声明数组

指针&数组

数组名同时也用作一个指针值,表示数组中首个元素地址

  • 如果编译器遇到数组变量名没带下标,则将其解释为指向数组 开始内存的指针变量

  • C++将数组视为指针最重要的原因:数组形参和实参共享

    • 数组和指针作为形参声明函数完全相同
    • 数组作为实参传递时,其值(首个元素地址)类似指针被 复制,调用函数中对数组的改变持久
    • 应该使用能反映其用途的方式声明参数,打算用数组作为 参数就声明参数为数组
  • C++中指针、数组最关键区别:变量声明时内存分配

    • 数组:连续的、可以存储数组元素的内存
    • 指针:存储机器地址的一个字的内存,不能直接存储数据
  • 指针作为数组使用

    • 将已存在数组首地址赋给指针有严格限制
    • 真正优势是程序运行时动态分配内存创建数组

动态内存管理

分配内存

  • 动态分配的内存在分配其的栈帧被释放后仍然保持
  • 动态内存分配一定要手动及时释放

new

new:以某种类型,从堆中分配一块空间给所指定类型的变量

  • new操作符返回堆中预留的、存储某类型值的地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int *ip = new int;
    // 从堆中分配单个类型空间给指定指针变量
    int *array = new double[4];
    // 从堆上给数组分配空间给指定指针变量
    // 动态数组
    Point *p_1 = new Point;
    // 从堆上给对象、结构体分配空间,调用默认构造函数
    Point *p_2 = new Point(2, 3);
    // 类型名后提供参数,则`new`会调用相应构造函数
  • 一旦在堆中分配了空间,可以通过解析指针来引用

xlloc

释放内存

delete

delete:取new操作符事先分配内存的指针,释放该指针指向 的内存空间

1
2
3
4
delete ip;
// 释放单个类型空间指针变量
delete[] array;
// 释放动态数组

释放内存策略

  • garbage collection:垃圾回收,自动查找不再使用的内存, 然后释放

拷贝

  • shallow copying:浅拷贝,C++默认拷贝

    • 如果值是指针,不会拷贝指针所指的值
    • 可以通过重载赋值操作符、构造拷贝构造函数改变默认的 浅拷贝行为
  • deep copying:深拷贝

    • 拷贝指针时,拷贝指针所指的值

关键字

Global Variable

const

const:常量,初始化之后不能改变

  • 优势

    • 描述性常量名使得程序更易于阅读
    • 大大简化程序日常中代码维护问题
  • const修饰经过依据优先级、其他关键字处理后的主体

    • 指针、值、参数:其他关键字处理得到目标语义主体处
    • 返回值:const置于函数签名头
    • 函数主体:函数体{}前、函数签名后
  • C++中常量声明中字符应全部为大写
  • 所以常量类成员必须在初始化列表中设置值

static

非成员静态变量

非成员静态变量:程序执行前既已在静态数据区分配内存

  • 静态全局变量

    • 未经初始化时会被自动初始化为0
    • internal属性,仅在声明文件内部可见,文件外不可见, 较一般全局变量不容易冲突
    • 普通全局变量:默认external,可以通过extern关键字 被其他文件访问,随机值初始化
  • 静态局部变量

    • 仅会在首次声明时被初始化,之后重复声明不会初始化
    • 具有static-storage duration/static extent,仅在 声明其的局部作用域中可见,但不会随函数栈退出而销毁, 适合函数重入需要保存局部状态场合

非成员静态函数

非成员静态函数:作用域仅限于声明文件

  • 仅在声明其的文件中可见,不能被其他文件使用,较一般函数 不容易发生冲突

    • 函数定义、声明默认extern,可通过extern关键字在 其他文件中使用

静态成员变量

静态成员变量:程序执行前既已在静态数据区分配内存

  • 静态成员变量属于类

    • 在内存中只有一份拷贝,所有类对象共享
    • 可以通过类名直接访问<cls>::<static_var>,若访问 权限允许
  • 只能在类中声明,在类外初始化

    • 初始化:<var_type> <cls>::<static_var> = <value>

静态成员函数

静态成员函数

  • 静态成员函数属于类

    • 没有this指针
    • 只能访问静态成员变量、静态成员函数
    • 可通过类名直接调用<cls>::<static_func>(),若访问 权限允许
  • 类内声明静态成员函数需要static关键字,在类外定义时 无需static关键字

extern

extern声明使用未定义(全局)变量、函数

  • extern修饰的变量可在当前文件、或其他文件中定义

    • 当前文件extern声明处后定义:扩展(全局)变量作用 范围
    • 在其他文件中定义:引用在其他文件中定义的(全局)变量
  • 函数声明中extern:仅表示函数可能在其他源文件中定义, 即extern总是可以省略

    • 因为函数声明默认无定义,而变量声明默认同时定义
  • extern声明要严格对应定义格式

    • extern char *a不能用于声明char a[6]
  • extern不能用于访问其他文件中静态全局变量
  • 编译器遇到extern函数、变量时在其他模块中寻找定义

external全局变量

  • 全局变量默认为external

    • 虽然作用域默认仅限于为当前文件,但是可以通过 external关键字在其他文件中声明访问
    • 可以通过static关键字使变量internal,在其他文件 中不可见
  • 程序中不能有多个同名非静态全局变量

    • 编译时:C++以文件为单位进行编译,可以有多个文件定义 同名非静态全局变量
    • 链接时:链接器将无法确定链接目标报错

头文件全局变量

  • 引入头文件相当于直接复制头文件内容,若在头文件中定义 全局变量

    • 头文件可以被多个源文件引入
    • 相当于在多个文件中定义同名全局变量报错
  • 头文件中不应定义非静态全局变量

    • 定义静态全局变量:在多源文件中定义不共享、互相独立、 不可见全局变量
    • 在头文件中声明extern全局变量:在某个源文件中仅定义 一次该全局变量,包含该头文件源文件共享该全局变量

extern "C"

extern "C":指定编译、链接规约,不影响语义

  • extern "C"是为C++编译器指定的

    • 编译C++文件时:对extern "C"声明的函数按照C编译规约 翻译名称、编译
    • 链接时:对extern "C"声明的函数按照C链接规约链接
  • "C":是指编译、链接规约,不是指C语言

    • 其他符合类C语言编译、链接规约的语言,如:Fortran、 Assembler,均可以使用extern "C"声明
  • C++支持重载,编译器会联合函数名、参数生成中间函数名, 在C++中使用C函数可能导致链接器无法到对应C函数
C++环境使用C函数

C++可以直接使用C函数接口,只是需要指明使用C函数

  • 使用extern "C"逐个声明C函数

    1
    2
    3
    4
    extern "C" void func(int a);
    extern "C"{
    void func(int a);
    }
    • 适合需要声明函数数量较少、分散在不同文件中
  • 在头文件中设置宏编译编译条件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #ifdef __cplusplus
    #if __cplusplus
    extern "C"{
    #endif
    #endif

    /*...c code...*/

    #ifdef __cplusplus
    #if __cplusplus
    }
    #endif
    #endif
    • 仅在C++源文件中有extern "C"声明,保证正常编译、 链接
    • 适合C函数声明集中在同一头文件中
C环境使用C++函数

C环境无法直接使用C++函数,必须转换为C函数接口才能使用

  • 通过extern "C"将C++中函数声明为C函数

    1
    2
    3
    extern "C" void func(int a);
    void func(int a);
    void func(int a, int b);
    • 带有extern "C"、不带是两个中函数声明,可以共存
    • 虽然C++中不能对重载函数声明extern "C",但C不支持 函数重载,也没有必要对重载函数声明extern "C"
  • 通过extern "C"创建包装C函数

    • 可以类似C++翻译重载函数名,将重载函数包装为多个C函数

      1
      2
      3
      4
      5
      void f(int);
      void f(doubel);

      extern "C" void f_i(int i){ f(i); }
      extern "C" void f_d(double d){ f(d); }
    • 可以包装类成员方法

      1
      2
      3
      4
      5
      6
      7
      class C{
      virtual doubel f(int);
      }

      extern "C" double call_C_f(C* p, int i){
      return p->f(i);
      }

register

register:寄存器变量,将局部变量值放在运算器的寄存器中

  • 存放在寄存器中的变量参与运算时,无需从内存中获取,节省 时间、提高效率

auto

auto:要求编译器对变量类型进行自动推导

  • auto声明变量必须初始化,以使编译器能够推导变量类型

    • 从此意义上说,auto是类型声明占位符,编译器在编译时 将auto替换变量实际类型
  • auto优势、用途

    • 声明有初始化表达式的复杂类型变量时简化代码
    • 避免声明变量类型时错误:编译器自动选择最合适类型
      • 函数返回值类型不确定,使用auto代替
    • 一定程度上支持泛型编程
      • 修改函数返回值类型时,无需修改代码
      • auto可以结合模板使用,加强泛型能力
  • C++11前,auto指具有自动存储期的局部变量,而没有声明为 static变量总是具有自动存储期的变量,使用频率极低
    • 具有自动存储期变量在进入声明该变量的程序块时被建立
    • 存在于程序块存活时,退出程序块时被销毁

注意事项

  • auto可以联合volatile*&&&使用

  • auto需要被推导为类型

    • 声明变量必须初始化
    • 不能和其他类型联合使用
    • 函数参数、模板参数不能被声明为auto
  • auto只是占位符,不是独立类型,不能用于类型转换或其他 涉及具体类型操作,如:sizeoftypeid

  • 定义在一个auto序列中变量必须推导为同一类型

  • auto不会被自动推导为constant&volatile qualifiers, 除非被声明为引用类型

    1
    2
    3
    const int i = 99;
    auto j = i; // `j`为`int`类型,不是`const int`
    auto& k = i; // `i`为`const int&`类型
  • auto会退化为指向数组的指针,除非被声明为引用

    1
    2
    3
    4
    5
    6
    7
    int a[9];
    auto j = a;
    count << typeid(j).name() << endl;
    // 输出`int*`
    auto& k = a;
    count << typeid(k).name() << endl;
    // 输出`int[9]`

decltype

decltype:要求编译器在编译时进行类型推导

  • 以普通表达式作为参数,返回表达式类型

    • decltype不会对表达式进行求值
  • decltype用途、优势

    • 推导表达式类型
    • using/typedef联合使用,定义类型
    • 重用匿名类型
    • 结合auto,追踪函数返回值类型

推导规则

  • e为无括号的标记符表达式、类成员访问表达式

    • decltype(e)e所命名的实体类型
    • e为被重载函数、或实体不存在,将导致编译错误
  • e类型为T

    • e为将亡值,则decltype(e)T&&
    • e为左值,则decltype(e)T&
    • e为纯右值,则decltype(e)T

volatile

  • volatile: a situation that is likely to change suddenly and unexpectedly

volatile:变量可能受到程序外因素影响,不应该对其做出 任何假设

  • C/C++中对volatile对象访问,有编译器优化上的副作用, 降低性能

    • volatile对象访问必须与内存进行交互,不能直接 使用寄存器中已有值
    • volatile对象相关代码块不允许被优化消失
    • 多个volatile对象访问保序编译器不可交换 对volatile对象访问的执行次序
      • 不能保证CPU乱序执行不交换执行次序
      • 因此volatile无法解决多线程同步问题
    • X86、AMD64等常用架构CPU只允许store-load乱序, 不允许store-store乱序,此时volatile用于线程同步 是安全的,但是这依赖于硬件规范,且会降低执行效率
    • 推荐使用原子操作、互斥量等实现
  • 用途

    • 信号处理相关场合
    • 内存映射硬件相关场合
    • 非本地跳转相关场合

错误线程同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
volatile bool flag = false;
thread1(){
flag = false;
volatile Type* value = new Type();
thread2(value);

while(true){
if(flag == true){
// 希望`thread2`更新完`value`后继续执行
// `flag`为`volatile`保证此语句块不会被优化消失
apply(value);
break;
}
}
thread.join();
if(nullptr != value){
delete value;
}
return;
}

thread2(volatile Type* value){
value->update();
flag = true;
// `flag`、`value`均为`volatile`保证两语句不会交换执行次序
return;
}
  • 以上代码即使对相关变量都设为volatile,也依赖CPU执行 规范才能保证安全

__unaligned

IO

  • C++中数据输入/输出操作是通过I/O流库实现

  • 流:数据之间的传输操作

    • 输出流:数据从内存中传送到某个载体、设备中
    • 输入流:数据从某个载体、设备传送到内存缓冲区
  • C++中流类型

    • 标准流I/O流:内存与标准输入、输出设备之间信息传递
    • 文件I/O流:内存与外部文件之间信息传递
    • 字符串I/O流:内存变量与表示字符串流的字符数组 之间信息传递

<ios>

class ios

ios:流基类

  • 所有流的父类
  • 保存流状态、处理错误

方法

  • .fail():判断流是否失效

    • 尝试超出文件的结尾读取数据时
    • 输入流中字符串无法被正确解析
  • .eof():判断流是否处于文件末尾

    • 基于C++流库语义,.eof方法只用在.fail调用之后, 用于判断错故障是否是由于到达文件结尾引起的
  • .clear():重置与流相关状态位

    • 故障发生后,任何时候重新使用新流都必须调用此函数
  • if(stream):判断流是否有效

    • 大部分情况下等同于if(!stream.fial())
  • .open(filename):尝试打开文件filename并附加到流中

    • 流方向由流类型决定:输入流对于输入打开、输出流对于 输出打开
    • 可以调用.fail判断方法是否失败
  • .close():关闭依附于流的文件

.[un]setf
1
2
UKNOWN setf(setflag, unsetfield);
UKNOWN unsetf(unsetflag);
  • 用途

    • .setf:设置某个流操纵符
    • .unsetf():取消某个流操纵符
  • 参数

    • setflag:需要设置的操纵符
    • unsetflag:取消设置的操纵符
    • unsetfield:需要清空的格式设置位组合
  • 不能像<<>>中省略操纵符ios::前缀
.rdbuf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class Elem, class Traits>
class basic_ios: public ios_base{
basic_streambuf <_Elem, _Traits> *_Mystrbuf,

_Mysb * rdbuf() const{
return (_Mystrbuf);
}

_Mysb * rdbuf(_Mysb * _Strbuf){
_Mysb * _Oldstrbuf = _Mystrbuf;
_Mystrbuf = _Strbuf;
return (_Oldstrbuf);
}
}
  • 用途:获得输入、输出流对象中指向缓冲区类streambuf指针
    • >><<操作符对其有重载,可以方便读取、写入

class istream

istream:输入流基类

  • 将流缓冲区中数据作格式化、非格式化之间的转换
  • 输入

方法

  • .unget():复制流的内部指针,以便最后读取的字符能再次 被下个get函数读取
.get
1
2
3
4
5
6
int_type get();
basic_istream& get(E& c);
basic_istream& get(E *s, streamsize n);
basic_istream& get(E *s, streamsize n, E delim);
basic_istream& get(basic_stream<E, T> &sb);
basci_istream& get(basci_stream<E, T> &sb, E delim);
  • 用途:从输入流中获取字符、字符串

  • 参数

    • delim:分隔符,缺省\n
    • n

(友元)函数

getline
1
2
3
4
5
6
7
8
9
10
11
12
template<class E, class T, class A>
basic_istream<E, T>& getline(
basic_istream<E, T>& is,
basic_string<E, T, A>& str,
);

template<class E, class T, class A>
basic_istream<E, T>& getline(
basic_istream<E, T>& is,
basic_string<E, T, A>& str,
E delim,
);
  • 用途:从流is读取以delim为界,到字符串中

    • 保留开头空白字符、丢弃行尾分割符
    • 读取字符直到分隔符,若首字符为分隔符则返回空字符串
  • 参数

    • delim:分隔符,缺省为换行符\n

class ostream

ostream:输出流基类

  • 将流缓冲区中数据作格式化、非格式化之间的转换,输出

方法

  • .put(ch):将字符ch写入输出流

class iostream

iosstream:多目的输入、输出流基类

Operator

Insertion Operator

<<:插入操作符,将数据插入流中

  • 左操作数是输出流

  • 右操作数是需要插入流中的数据

    • 基本类型:<<会将其自动转换为字符串形式

      • 整形:默认10进制格式
      • [unsigned ]char类型:总是插入单个字符
    • streambuf类型指针:插入缓冲区对象中所有字符

Extraction Operator

>>:提取操作符,从输入流中读取格式化数据

  • 左操作数为输入流

  • 右操作数存储从输入流中读取的数据

    • 缺省

      • skipws:忽略开头所有空白字符
      • 空白字符分隔:读取字符直到遇到空白字符
    • streambuf类型指针:把输入流对象中所有字符写入该 缓冲区

  • 几乎不提供任何支持检测用户输入是否有效的功能
    • 数据格式由变量类型控制

缓冲

缓冲类型

  • ISO C要求
    • 当且仅当不涉及交互设备时,标准输入、输出全缓存
    • 标准错误绝不是全缓存
  • 无缓冲:不缓冲字符

    • 适用情况:标准错误

    • 标准库不缓冲不意味着系统、设备驱动不缓冲

  • 行缓冲:在输入、输出遇到换行符时才会执行I/O操作

    • 适用情况:涉及交互设备,如标准输入、输出
  • 全缓冲:I/O操作只会在缓冲区填满后才会进行

    • 适用情况:大部分情况,如驻留在磁盘的文件

    • flush描述I/O缓冲写操作

      • 标准I/O函数自动flush
      • 手动调用对流调用死fflush函数
  • 缓冲区一般是在第一次对流进行I/O操作时,由标准I/O函数调用 malloc函数分配得到

文件自定义缓冲区

  • 文件必须已打开、未做任何操作
setbuf
1
void setbuf(FILE * restrict fp, char * restrict buf);
  • 用途:打开或关闭缓冲区
    • 打开:buf必须为大小为BUFSIZ的缓存
      • BUFSIZ:定义在stdio.h中,至少256
    • 关闭:将buf设置为NULL
setvbuf
1
2
int setvbuf(FILE * restrict fp, char * restrict buf,
int mode, size_t size);
  • 用途:设置缓冲区类型

流自定义缓冲区

setbuf
1
virtual basic_streambuf * setbuf(E *s, streamsize n);

Manipulator

(流)操纵符:控制格式化输出的一种特定类型值

输出

  • 短暂的:只影响下个插入流中的数据
  • 持久的:直到被明确改变为止

  • 双操纵符条目中,前者为默认

  • setwsetprecisionsetfill还需要包含<iomanip>

组合格式

  • adjustfield:对齐格式位组合
  • basefield:进制位组合
  • floatfield:浮点表示方式位组合

位置

  • endl:将行结束序列插入输出流,确保输出字符被写入目的流
  • setw(n):短暂的
  • setfill(ch):持久的,指定填充字符,缺省空格
  • left:持久的,指定有效值靠左
  • right:持久的,指定有效值靠右
  • internal:持久的,指定填充字符位于符号、数值间

数值

  • showbase:为整数添加表示其进制的前缀
  • fixed:持久的,完整输出浮点数
  • scientific:持久的,科学计数法输出浮点数
  • setprecision(digits):持久的,精度设置依赖于其他设置

    • fixed/scientific:指定小数点后数字位数
    • 其他:有效数字位数
  • hex:持久的,16进制输出无符号整形

  • oct:持久的,8进制输出无符号整形
  • dec:持久的,10进制输出整形

  • noshowpoint/showpoint:持久的,否/是强制要求包含 小数点

  • noshowpos/showpos:持久的,要求正数前没有/有+
  • nouppercase/uppercase:持久的,控制作为数据转换部分 产生任意字符小/大写,如:科学计数法中的e
  • noboolalpha/boolalpha:持久的,控制布尔值以数字/ 字符形式输出

控制

  • unitbuf:插入、提取操作之后清空缓冲
  • stdio:每次输出后清空stdout、stderr

输入

  • skipws/noskipws:持久的,读取之前是/否忽略空白字符
  • ws:从输入流中读取空白字符,直到不属于空白字符为止

<iostream>

  • ifstream_withassign:标准输入流类

    • cin:标准文件stdin
  • ofstream_withassign:标准输出、错误、log流

    • cout:标准文件stdout
    • cerr:标准文件stderr
    • clog:标准文件stderr

<fstream>

  • ifstream:文件输入流类

    • 默认操作:ios::in
  • ofstream:文件输出流类

    • 默认操作:ios::out|ios::trunc
  • fstream:文件流输入、输出类

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <fstream>

int main(){
ifstream infile;
ofstream outfile;
// 声明指向某个文件的流变量

infile.open(filename)
// 打开文件:在所声明变量和实际文件间建立关联

infile.close()
// 关闭文件:切断流与所关联对象之间联系
}

流操作复制文件

  • 逐字符复制

    1
    2
    3
    4
    5
    6
    7
    #include<fstream>
    std::ifstream input("in", ios::binary);
    std::ofstream output("out", ios::binary);
    char ch;
    while(input.get(ch)){
    output << ch;
    }
    • 使用input >> ch默认会跳过空白符,需要使用 input.unsetf(ios::skipws)取消
  • 逐行复制

    1
    2
    3
    4
    5
    #include<string>
    std::string line;
    while(getline(input, line)){
    output << line << "\n";
    }
    • 若文件最后没有换行符,则复制文件会末尾多\n
  • 迭代器复制

    1
    2
    3
    4
    5
    6
    #include<iterator>
    #include<algorithm>
    input.unsetf(ios::skipws);
    copy(istream_iterator(input), istream_iterator(),
    ostream_iterator(output, "")
    );
  • 缓冲区复制

    1
    output << input.rdbuf();
    • 丢失\n

标准输出文件内容

  • <<操作符

    1
    2
    3
    4
    5
    #include<iostream>
    #include<fstream>

    ifstream input("in");
    cout << input.rdbuf();
  • .get方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    while(input.get(*cout.rdbuf()).eof()){
    // 读取一行
    if(input.fail()){
    // `get`遇到空行无法提取字符,会设置失败标志
    input.clear();
    // 清除错误标志
    }
    cout << char(input.get());
    // 提取换行符,转换为`char`类型输出
    }
  • .get方法2

    1
    input.get(*cout.rdbuf(), EOF);

<sstream>

  • 基于C类型字符串char *编写

    • istrstream:串输入流类
    • ostrstream:串输出流类
    • strstream:串输入、输出流类
  • 基于std::string编写:推荐

    • istringstream:串输入流类
    • ostringstream:串输出流类
    • stringstream:串输入、输出流类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sstream>

int string_to_integer(string str){
instringstream istream(str)
// 类似`ifstream`,使用流操作符从字符串中读取数据
int value;

istream >> value >> ws;
// `>>`忽略流开头空白字符,`ws`读取尾部空白
if(stream.fail() || !stream.eof()){
// 如果字符串不能作为整数解析,`.fail`返回`true`
// `.eof`返回`false`,说明字符串包含其他字符
error("string to integer: illegal integer format");
}
return value;
}

string integer_to_string(int n){
ostringstream ostream;
ostream << n;
return stream.str();
//
}

class

类:包含值、相应操作集的模板,将相关信息片段组织成复合值, 使得可以整体对其进行操作

概念

实体

  • object:对象,属于一个类的所有值

  • instance:实例,单个对象

  • field/instance variable:域/实例变量,类的分量

  • method:应用于类实例的操作

    • C++中方法的使用和传统函数类似,新名称只是为强调其同 所属的类紧密联系
    • 传统函数称为free function,不被约束于特定类

权限

  • private:私有,声明在private section中的域仅对该类 本身可见(默认)

  • public:公有,声明在public section中的域对所有用户 可见

    • 现代面向对编程不鼓励在类中声明public实例变量
  • protected:受限,声明在protected section中的成员对 所有子类都可以访问,但用户不能访问

类&结构体

C++中:结构类型和类基本上以同样方式实现,类是struct的扩展

  • 结构体:默认访问权限是public,类默认为private
  • 类:允许高级继承、方法,成员默认private
  • 对一般成员变量,类和结构体在内存布局上完全一致,如: 顺序、内存布局

消息传递模型

面向对象程序设计中,对象间通过信息发送、请求实现对象间通信, 将传递的这些信息称为消息

1
receiver.name(arguments)
  • 对象间消息发通常理解为一个对象调用另一个对象的方法
  • sender:发送方,初始化方法的对象
  • receiver:接收方,消息的目标对象

接口实现分离

  • C++中类接口、实现相分离时,类自身定义仅存在与其.h文件

  • 实现放在.cpp中作为独立方法定义,需要以类名作为限定符、 ::作为分隔的方式表明自己所属的类

类继承

1
2
3
class subclass: public superclass{
...code...
}
  • 子类subclass继承父类superclass所有publicprotected成员,private成员仍然保持私有特性

  • 可以继承模板类

    • 子类甚至可以不包含任何代码,仅为简化类型名
      1
      2
      class StringMap: public Map<string, string>{}
      // 创建不包含代码的子类,简化代码

子类局限

子类对象是其所属父类的实例,但是有其局限

  • 将子类对象赋值给父类对象会导致子类特有实例变量值被丢弃 (因为父类对象在栈中空间较小)

  • 常用解决方法是使用指针,指针变量大小相同

    • 但使用指针会使内存管理变得复杂
    • 为类定义析构函数可以管理内存,但是指针越界时,不能 保证析构函数会在合适的时间调用
  • 最好方法是避免使用类继承,并创建独立的类管理自身堆内存, 否则

    • 完全禁止拷贝:定义私有拷贝构造函数、重载赋值操作符, 但这会使得对象难以嵌入大型数据结构
    • 执行深拷贝:用户需要承担内存管理,重载拷贝构造函数、 重载赋值操作符

Multiple Inheritance

多重继承:类可以继承自多个父类

  • 多重继承在实际编程过程中可能导致程序复杂、模糊不清
    • 多重继承的多个父类可能拥有多个相同方法名、数据域名
  • C++中单继承已经足够复杂,最好避免使用多重继承

final

final:指定虚函数不能被派生类覆盖、类不能被继承

  • 语法

    • 虚函数:在声明符之后
    • 类:紧跟类名后
    1
    2
    3
    4
    5
    6
    7
    struct A final{}
    struct B{
    virtual void foo();
    }
    struct C: B{
    void foo() final;
    }

override

override:指定派生类虚方法覆盖基类虚方法

  • 语法:在成员函数声明之后 (其他情况下override甚至不是关键字)

  • 用途:有助于防止代码出现意外继承行为

    • 即不是强制性关键字,是辅助性、可选
    • 非虚函数可以通过类型转换调用基类方法,不算覆盖???

Iterator

迭代器:指向集合中一个特定元素,每次可以通过单步递进方式访问 其他元素

  • 迭代器使用*操作查找其指向的值

迭代器层次结构

iterator_hierarchy

  • InputIterator:允许读值
  • OutputIterator:允许给解析的迭代器赋新值
  • ForwardItertor:结合InputIteratorOutputIterator ,允许读写值
  • BidirectionIterator:在ForwardIterator基础上允许向后 迭代,增加--操作符
  • RandomAccessIterator:在BidirectionIterator基础上 允许向前、向后移动任意元素,包含全部关系符

指针作为迭代器

  • 指针类型已经实现了RandomAccesIterator提供的所有操作符 ,所以可以使用指针作为迭代器

  • 很多实现已经将iterator作为类中嵌套迭代器的类型,要用 typedef重命名指针类型为iterator

    1
    typedef ValueType * iterator;
    • 如:编译器内部利用迭代器,将基于范围for展开
  • 编译器内部iterator类型指针变量参见 cppc/basics/intro

类方法

Constructor

构造函数:创建对象

  • 构造函数名与类名完全一样

  • 构造函数无法被子类继承,只能在初始化列表中调用

    • 缺省编译器调用父类默认构造函数
    • 若没有构造函数,则必须显式在初始化列表中初始化

执行阶段

  • 初始化阶段:初始化列表

    • 调用父类构造函数初始化父类成员变量

      • 缺省调用默认构造函数
      • 若父类没有默认构造函数,必须在初始化列表中显式 调用构造函数
    • 初始化类自身数据域

      • 对一般类型,缺省类似于普通未赋值初始化
      • 对类类型成员变量,缺省没有显式初始化则调用默认 构造函数
  • 计算阶段:执行构造函数体

    • 在函数体中执行的都是赋值,不是初始化

Initializer List

初始化列表:初始化父类、自身数据域

  • 位于的构造函数体花括号前、参数列表之后,用:和参数列表 分隔
初始化父类
  • 父类名后用括号括起来的参数列表,参数列表必须和父类 某一构造函数原型相匹配

    1
    Foo(string name, int id):(name, id){};
  • 父类数据域后用括号括起的该数据域的初始化值(此方法 也可以用于初始化类自身数据域)

    1
    Foo(string name, int id): name(name), id(id){};
  • 调用父类构造函数

    1
    Derived(string name, int id):Base(name, id){};
初始化自身数据域
  • 类类型成员变量最好使用初始化列表初始化,可以避免 构造函数初始化阶段调用成员变量默认构造函数

  • 必须放在初始化列表中成员变量

    • 常量成员:常量只能初始化,不能赋值
    • 引用类型:引用必须在定义时初始化,不能重新赋值
    • 没有默认构造函数的类类型
  • 成员变量按照其在类中声明顺序初始化,而不是在初始化 列表中的顺序

    • 为避免因成员变量初始化依赖导致的未定义,应该按照成员 变量声明的顺序初始化

Default Constructor

默认构造函数:没有参数的构造函数

  • 若类没有默认构造函数,编译器会自行定义

Copy Constructor

1
Foo(const Foo &f){};

拷贝构造函数:使用同类实例初始化创建新对象

  • 用途

    • 复制对象
    • 传值调用时,隐式调用拷贝构造函数构造新对象传参
  • 类中没有定义拷贝构造函数时,编译器会自行定义一个

    • 若类中有指针变量、并有动态内存分配,则必须定义拷贝 构造函数

explicit

epxplicit关键字:声明为explicit构造函数不能在隐式转换中 使用

  • 用途
    • 希望函数参数只能是给定类型,可以禁止隐式类型转换

Destructor

析构函数:类的对象消亡时,析构函数被自动调用

  • 析构函数可以完成各种清理操作
    • 最重要:释放对象所创建的所有堆内存
    • 关闭对象打开的任何文件
  • 析构函数名称:类名前加上~
  • 析构函数特点
    • 没有返回类型
    • 不能重载
    • 每个类只有一个无参的析构函数
  • 良好设计的C++应用中,每个类都需要堆其对内存负责

Operator Overloading

操作符重载:扩展标准操作符以适应新的数据类型

  • 编译器遇到操作符时,会根据操作符操作数的类型确定其 操作语义

  • 重载操作符函数名由关键字operator后跟操作符构成

    • 操作符的左右操作数类型、传值方式
    • 操作符返回值
      1
      2
      3
      ostream & operator<<(ostream & os, Point pt);
      // 流对象不能拷贝,必须引用传递
      // 返回依然是流引用
  • 有些类型根本没有定义过某些操作符,虽然实际上是扩展该类型 操作符,但是也成为重载操作符

  • overload:重载,使用相同名字的不同版本类方法

重载方式

C++提供两种机制用以重载内置操作符使得可以适用于新定义类

  • 类方法:在类中用方法重载操作符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // .h
    Class Point{
    Public:
    bool operator==(Point rhs);
    // 类中声明操作符重载方法
    }

    // .c
    bool Point::operator==(Point rhs){
    // 实现中限定为`Point`方法
    }
    • 左操作数为该类型对象(声明、实现省略)
    • 右操作数作为形参传递
    • 编译器把左操作数视为接收者,将右操作数作为形参传递
  • free function在类外使用自由函数重载定义操作符

    1
    2
    3
    4
    5
    6
    // .h
    bool operator==(Point pt1, Point pt2);

    // .c
    bool operator==(Point pt1, Point pt2){
    }
    • 二元操作符的两个操作数都作为形参传递
    • 操作符重载自由函数一般需要声明为类的友元函数,使其 能够访问类的私有实例变量

++/--

重载++/--时,必须指明是重载前缀还是后缀形式

  • 额外传入无意义整形参数说明重载后缀形式
  • 常用于枚举类型,方便遍历枚举类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Direction operator++(Direction & dir){
// 重载前缀形式
dir = Direction(dir+1);
return dir;
}

Direction operator++(Direction & dir, int){
// 重载后缀形式
// 这也说明,C++中若不需要使用形参值,可以省略形参名,
// 即使是在函数实现中
Direction old = dir;
dir = Direction(dir + 1);
return old;
}

=

  • 赋值操作符被定义为返回其左值,所以重载赋值操作符时注意 返回值

  • 赋值操作符开头往往检查左、右操作数地址是否一样

    • 避免不必要拷贝操作、逻辑错误

()

  • 重载函数调用操作符()将可以像函数一样调用对象
1
2
3
4
5
6
7
8
9
10
11
class AddFunction{
Public:
AddFunction(int k){
this->k = k;
}
int operator(int x){
return x+k;
}
private:
int k;
}
  • 重载此方法类称为function class(函数类),其实例称为 function object/functor(函数对象、函数子)

const方法

C++允许在方法参数表后增加关键字const指定方法,声明 其不改变其对象的状态

  • 在接口原型、实现中都应该使用const关键字约束
1
2
3
4
5
6
7
// .h
int size() const;

//.cpp
int CharStack::size() const{
return count;
}

Friend

友元:允许访问类中私有实例变量的元素(不是类方法)

  • 友元函数:允许访问类中私有变量的自由函数

    1
    2
    3
    4
    5
    friend prototype;
    // 在类定义中声明友元函数
    class Point{
    friend bool operator==(Point p1, Point p2);
    }
  • 友元类:允许访问类中私有实例变量的类

    1
    2
    friend class cls_name;
    // 在类中定义中声明友元类
    • 友元类的声明不是双向的,需要两个类都显式声明另一个类 为友元类,才能相互对方私有变量

典型方法

  • Accessor/Getter:访问器/读取器:获取实例变量值的函数

    • 为方便,读取器的命名通常以get为前缀
  • Mutator/Setter:设值方法/设值器,为特定实例变量 设置值的方法

    • 为方便,设置器的命名通常以set为前缀
    • 将实例变量设为私有就是为了阻止用户不受限制访问变量, 所以读取器的设置更为常见
    • 事实上,immutable设计风格将类设置为完全不可变

类内存结构

  • 类的方法地址不会存储在实例的内存空间中

非虚函数

  • 编译时

    • 编译器根据变量类型(指针、引用类型)在调用函数 处写入类方法的地址
    • 编译时即确定执行的函数
    • 可以认为是普通函数,只是默认包含参数this指针
  • 运行时

    • 访问方法相应内存地址,传入this指针调用方法

(Pure)Virtual Method

  • 虚方法/虚函数:virtual修饰的方法,基类中有定义
  • 纯虚方法:没有函数体的虚函数,基类中无定义,实现只能由 其子类提供
  • 派生类中覆盖方法总为虚方法,无论是否有virtual关键字
    • 建议后代虚函数加上virtual以提升可读性
1
2
3
4
virtual double getpay();
// `virtual`:普通虚函数
virtual double getPay() = 0;
// `=0`:纯虚函数,没有函数体
  • 编译时

    • 编译器根据类的声明创建虚表,每个类维护一张虚表
    • 对象被构造时,虚表地址在实例内存的首个字
    • 编译器在调用函数处不直接写入函数地址(因为不确定 调用的函数)
  • 执行时

    • 从对象首读取虚表,在虚表中查询需要执行方法地址
    • 访问相应方法地址,传入this指针调用方法
    • 实现多态:subtyping polymorphism,同一语句具体调用 方法动态决定
  • 多态:参见program/program_design/language_design
  • vTable:虚表,存储一个类中所有虚函数地址
    • 虚表是依次存储当前类中所有虚方法,不是继承序列 中同一个虚方法

Abstract Class

抽象类:包含纯虚方法的类

  • 抽象类主要作用

    • 将有关类组织在继承层次结构中

    • 刻画一组子类的操作接口的通用语义,只描述派生类共同 操作接口,完整实现留给子类

  • 抽象类只能作为基类派生新类使用,不能创建抽象类对象

    • 所以抽象类也不能做参数类型、函数返回类型、显式转换 类型

    • 可以定义指向抽象类的指针、引用,可以指向其派生类, 进而实现多态

    • 此特性本身就是为了解决基类生成对象不合理的问题, 抽象类就是为了抽象设计目的而建立

    • 实际中,为了强调类是抽象类,可以将类的构造函数 放在protected区域

    • 其派生类必须实现基类中所有纯虚函数,才能成为非抽象类 ,否则仍然是抽象类

  • 构造/析构函数内不能使用纯虚函数(一般成员方法内可以)

    • 因为派生类构造/析构函数内会调用基类构造/析构函数

    • 纯虚函数没有函数体,不能被调用

Override

覆盖/重置:在派生类中签名一样的方法会覆盖父类的方法

  • 非虚方法:调用方法取决于指针、引用类型

    • 将派生类对象赋给基类指针、引用后,调用方法是基类方法 ,表现父类的行为
  • 虚方法:动态检查,调用方法取决于对象实际类型

    • 将派生类对象赋给基类指针、引用后,调用方法是派生类 方法
    • 即指针(引用)根据指向值决定行为,实现多态

Template

模板:parameterized class参数化类,包含base type规格说明 的类

  • 模板是C++多态的一种实现

  • 编译器遇到函数模板调用,会自动生成相应版本函数拷贝

    • 因此模板必须能实现,即不能将模板接口、实现分开, 否则编译器无法生成相应版本函数拷贝 (当然可以通过#include强行将接口、实现分开)
    • 模板不能节省空间

模板使用

在函数、类前添加

1
template <typename ValueType>
  • template关键字:表示此行后整个语法单位是模板模式一部分
  • ValueType:类型占位符,生成函数拷贝时被替换为相应类型

模板函数

1
2
3
4
template <typename ValueType>
ValueType max(ValueType x, ValueType y){
return (x > y) ? x: y;
}

模板类

1
2
3
4
5
6
7
8
9
10
11
// .h
template <typename T1, typename T2>
class Stack{
T1 d1;
T2 d2;
}

template<typename ValueType>
Stack<ValueType>::Stack(){
}
// 模板实现、接口在同一文件中

Template Specialization

模板特化:指定一个或多个有具体模板参数的模板

  • 全特化:给出所有模板参数
  • 偏特化:给出部分模板参数
  • 模板实例化时,优先使用模板参数最匹配的模板版本

  • 通过特化模板,可以对特定模板参数集合自定义当前模板

    • 最好特化模板的接口和普通模板一致

类模板特化

类模板可以全特化、偏特化

  • 特化的模板参数从模板参数列表中移除
  • 在类名后给出完整参数列表
1
2
3
4
template <typename T2>
class Stack<int, T2>{
...
}

函数模板特化

函数模板只能全特化

  • 若编译器可以通过返回值类型推断模板实参类型,可以省略 函数后模板参数列表,否则引起歧义报错
  • 函数模板“偏特化”可以通过函数模板重载实现
1
2
3
4
template <>
int max(int x, int y){
...
}

C++库结构

定义C++库时,需要提供:interfaceimplementation

  • 类库向用户提供了一组函数、数据类型,以实现 programming abstraction

  • 类库像函数一样,提供了可用于降低复杂度的方法,但也需要 在建库时考虑更多细节,简化程度取决于接口设计的优劣

接口、实现

  • 接口:允许库用户在不了解库实现细节的情况下使用库中库函数

    • 典型接口可提供多种定义、声明,称为interface entry
      • 函数声明
      • 类型定义
      • 常量定义
  • 实现:说明库的底层实现细节

  • C++接口通常写在.h头文件中,实现在同名.cpp文件

接口设计原则

  • unified:统一性,接口必须按照统一主题来定义一致的抽象
  • simple:简单性,接口必须向用户隐藏实现的复杂性
  • sufficient:充分性,接口必须提供足够功能满足用户的需求
  • general:通用性,良好设计的接口必须有高度适用性
  • stable:稳定性,接口在函数底层实现改变时也要有不变的 结构、功能

使用

1
2
3
4
5
$ g++ -o a.out src.cpp -L /path/to/library -l lib_name
// 动态、静态链接库均可
// `-L`指定(额外)库文件搜索路径
$ g++ -o a.out src.cpp liblib_name.a
// 静态链接库可以类似于`.o`文件使用
  • 编译时使用指定链接库名只需要指定lib_name,编译器自动 解析为lib[lib_name].so

  • gcc/ld为可执行文件链接库文件时搜索路径

    • /lib/usr/lib64/usr/lib/usr/lib64
    • LIBRARY_PATH中包含的路径

静态链接库.a/.lib:二进制.o中间目标文件的集合/压缩包

  • 链接阶段被引用到静态链接库会和.o文件一起,链接打包到 可执行文件中
  • 程序运行时不再依赖引用的静态链接库,移植方便、无需配置 依赖
  • 相较于动态链接库浪费资源、空间
  • 相较于.o二进制的文件,管理、查找、使用方便

生成静态链接库

  • linux下使用ar、windows下使用lib.exe,即可将目标文件 压缩得到静态链接库

  • 库中会对二进制文件进行编号、索引,以便于查找、检索

    1
    2
    3
    $ g++ -c src.cpp src.o
    $ ar -crv libsrc.a src.o
    // 生成静态库`libsrc.a`
  • linux下静态库命名规范:lib[lib_name].a(必须遵守,因为 链接时按此规范反解析名称)

动态链接/共享库.so/.dll

  • 动态链接库在程序链接时不会和二进制中间文件一起,被打包 进可执行文件中,而时在程序运行时才被 dynamic linker/loader载入

  • 不同程序调用相同库,在内存中只需要该共享库一份实例,规避 空间浪费、实现资源共享

  • 解决了静态库对程序更新、部署、发布的麻烦,可以通过仅仅 更新动态库实现增量更新

  • 执行环境需要安装依赖、配置环境变量(或者是编译时指定依赖 搜索路径)

生成动态链接库

  • 直接使用编译器即可创建动态库

    1
    2
    3
    4
    5
    $ g++ -f PIC -c src.cpp -o src.o
    # PIC: position independent code
    # 创建地址无关的二进制目标文件w
    $ g++ -shared -nosname libsrc.so -o libsrc.so.1 src.o
    # 生成动态链接库
  • 动态链接库命名规则:lib[libname].so(必须按照此规则 命名,因为链接时按照此规则反解析库名称)

dynamic linker/loader

动态载入器:先于executable模块程序工作,并获得控制权

  • 对于linux下elf格式可行程序,即为ld-linux.so*
  • 按照一定顺序搜索需要动态链接库,定位、加载

搜索次序

ld-linux.so*依次搜索以下,定位动态链接库文件、载入内存

  • elf文件的DT_RPATH段:链接/编译时指定的搜索 库文件的路径,存储在elf文件中

    • g++通过添加-Wl,rpath,、ld通过-rpath参数指定添加 的路径
    • 若没有指定rpath,环境变量LD_RUN_PATH中路径将被添加
    1
    2
    3
    4
    5
    6
    7
    8
    $ objdump -x elf_exe
    # 查看elf文件的`DT_RPATH`
    $ g++ -Wl,-rpath,/path/to/lib
    # 在g++命令中直接给出链接参数
    # 也可以使用链接器`ld`链接时给出
    $ g++ -Wl,--enable-new-tags,-rpath,'$ORIGIN/../lib'
    # 使用相对路径
    # Makefile中使用时需要转义`$$ORIGIN/../lib`
  • LD_LIBRARY_PATH环境变量

    • 优先级较高,可能会覆盖默认库,应该避免使用,会影响 所有动态链接库的查找
    • 不需要root权限,同时也是影响安全性
  • /etc/ld.so.cache文件列表(其中包括所有动态链接库 文件路径)

    • /lib/lib64/usr/lib/usr/lib64隐式 默认包含,优先级较低、且逐渐降低

    • 其由ldconfig根据/etc/ld.so.conf生成,库文件添加 进已有库路径、添加路径至/et/ld.so.conf后,需要通过 ldconfig更新缓存才能被找到

    • ldconfig具体参见linux/shell/cmd_sysctl
  • 因为LD_LIBRARY_PATH的缺点,建议使用LD_RUN_PATH,在 链接时就指定动态库搜索路径

STL 其他