跳至主要內容

C++ 学习笔记

2023年8月11日大约 47 分钟

C++ 学习笔记

主要参考书籍 C++PrimerPlus

拾遗

运算符

特殊运算符

符号功能优先级示例
sizeof()获取值所占用的 (栈) 空间, 单位为字节与逻辑非, 正负号等一元运算符同为第二高优先级sizeof((int)1) = 4
A ? B : C 条件运算符表达式 A 为真时运行 B 否则运行 C, 表达式 B, C 的结果类型必须相同优先级倒数第三 (低于 <<, 因此在 cout 中最好加括号)1?2:3 = 2
A ,B 逗号运算符将多个表达式用逗号隔开, 总的结果为最后一个表达式的结果优先级最低1,2 = 2
a = B, +=, *= , ... 等赋值运算符返回被赋值变量的引用优先级倒数第二(a = 1) += 5 = 6 (&a)
a++, ++a 自增运算符均产生使 a 加一的副作用, 其中 a++ 返回 &a, 而 ++a 返回值为 a + 1 的常量, 将 + 替换为 - 同理a++ 为最高优先级, ++a 为第二高优先级a++ + a++ = a * 2 + 1, 注意同级别的表达式将会分别计算 (顺序不确定, 因此最好避免此类用法)

特殊知识

标准输入输出

cout 输出格式控制

cin 读取控制

可变参数

使用可变参数实现传入任意长度的参数

  1. 可变参数教程open in new window
  2. 通过 vsprintf 函数, 实现通过可变参数, 对 sprintf 再封装 eg.
va_list p;
va_start(p, format);
_offset += vsprintf((char*)(_buf + _offset), format, p);
va_end(p);

组合变量类型

常量指针

const char* / char const* (等价)
表明指针指向一个常量, 因此指针所指向的值不能更改, 但指针变量储存的地址可以改变(也可进行 + 等操作)

const char* p = "ABC";
p = "CDF";//正确
p++;//正确

指针常量

char *const 指针为一个常量, 即指针变量储存的地址不能改变

char const* p = "ABC";//不正确, 不能将const char*赋给char*
char str[] = "ABC";
char const* p = str;//正确
p++;//不正确
常量指针常量

两种类型的组合, 即 const char* const

常量引用

const int& 引用关系一旦建立后无法修改, 因此不存在引用常量
常量引用表示对于常量的引用, 无法修改引用指向的值

常量常量指针引用

定义宏

通过 #define [名称] [表达式]#define [名称]([参数, ...]) [表达式] 创建宏
在使用宏的时候, 为了避免由运算顺序导致的错误, 最好对宏中的各个要素加上 ()

调试宏

宏运算符

杂项知识

输入输出流

stdout, stdin, stderr 分别表示是标准输出,标准输入和标准错误, 并对应 cout, cin, cerr
stdout 是行缓冲的(换行时缓冲), stderr 是无缓冲的.
以此对于以下代码

int main(){
    fprintf(stdout,"Hello ");
    fprintf(stderr,"World!");
    return 0;
}

将输出结果 World!Hello
详细可参考open in new window

程序控制

格式化字符串

输出格式化字符串
其他参考

第九章

头文件 P247

头文件包含的内容

  1. 函数原型
  2. #defineconst 定义的常量
  3. 结构, 类, 模板声明
  4. 内联函数

include

  1. 包含头文件使用双引号
  2. 头文件中使用 #ifndef (注意中间的n) #define 来避免多次包含同一个头文件 不能忽视, 特别是hpp文件中

变量作用域 P250

自动存储持续性

int main()
{
    int a = 10;
    int b = 20;
    {
        int a = 30;
        int c = 40;

		cout <<"In Block" << endl;
        cout <<"a: " << a << endl;
		cout <<"b: " << b << endl;
		cout <<"c: " << c << endl;
	}
	cout <<"Out of Block" << endl;
	cout <<"a: " << a << endl;
	cout <<"b: " << b << endl;
	// cout <<"c: " << c << endl;
    return 0;
}

静态变量

  1. 在代码块外定义(外部链接性) 能够被所有代码访问(跨CPP)
  2. 在代码块外定义 加上 static 关键字(内部链接性) 只能在定义该变量的文件中使用 3, 在函数内定义 加上 static 关键字(无连接性) 只能在定义该变量的代码块中使用
跨文件说明

在编译时, 各个头文件的内容会被扩展到源文件内, 此处跨文件指多个同时编译的源文件

静态变量初始化

默认为 0

引用声明 P255

引用来自其他源文件的全局变量时, 需要使用 extern [变量定义], 或者使用 ::[全局变量] 表示使用全局版本的变量

extern 的说明
不使用 extern 的后果
  1. 直接使用其他文件的全局变量 由于变量未声明直接使用, 将出错
  2. 不使用 extern 编译器认为要定义一个新的全局变量, 由于变量名重复, 将出错

隐藏外部全局变量 P258

不使用外部的全局变量时, 可以用 static 声明作用域更小的全局变量
此时将优先使用作用域更小的全局变量 (隐藏其他源文件的全局变量, 防止变量重复)

无连接性的静态变量

在程序启动时进行一次初始化, 之后保持不变

其他说明符/限定符 P260

同一个声明不能使用多个说明符(thread_local 除外)

  1. volatile 表明内存单元没有程序修改也可能发生改变
    用于指向硬件位置的指针
  2. mutable 指出类变量为 const 时, 有 mutable 的成员依然可变 (可用于类中记录调用次数的成员等)

const 说明符

全局 const 的连接性为内部(同 static)

使用内部链接的意义

函数的连接性

默认情况下函数为外部链接性

  1. 这意味着两个源文件中, 不能有同名函数
  2. 可以使用 extern 访问其他源文件中的函数 (extern 可省略, 没有函数体的函数即声明)
  3. 可以使用 static 使函数的连接性为内部
内联函数

内联函数不受此规则限制, 这表明内联函数可以定义在头文件内

动态存储变量 P262

new初始化

  1. 构造函数 int *p = new int (6);
  2. 初始化结构/数组 int *p = new int [4] {1, 2, 3, 4};

定位new运算符

#include<new> 后, 可以指定 new 的位置
eg.

#include<new>
int buffer[256];
int main()
{
	int *p = new(buffer) int[16];
}

此时将从 buffer 中分配空间给 p, 由于空间为静态, 不需要 delete

delete

  1. 使用 new 初始化的指针必须使用 delete 释放空间
  2. 使用 new[] 初始化的指针必须使用 delete[] 释放空间

名称空间 P266

名称空间中的定义与声明

使用 namespace [名称空间] {} 表示内部的代码使用指定的名称空间 由于名称空间不能在代码块中, 因此名称空间中的变量通常为外连接性 允许多个使用相同名称空间的 namespace 存在
eg

namespace test
{
	int fun();
}
namespace test
{
	int fun()
	{
		return 1;
	}
}

全局名称空间

一般的全局变量在全局名称空间中 因此除了使用 extern 声明全局变量, 也可以使用 ::[全局变量], 直接访问全局变量

using声明

namespace test
{
	int a;
}
int main()
{
	using test::a; // 将a导入局部区域(不是全局)
	int a;// 由于a已在局部区域存在, 将出错
	return 1;
}

using编译指令

namespace test
{
	int a;
}
int main()
{
	using namespace test; // 将a导入全局区域(不是全局)
	int a;// test::a被局部的a覆盖, 不会出错
	return 1;
}

名称空间其他特性

namespace a
{
	int i;
}
namespace b
{
	using a;
}

此时 a::ib::i 等价

第十章 P279

基础

class与struct

内联方法

除了 inline, 在函数声明内定义的成员函数默认为内联函数

构造函数中防止参数冲突

构造函数的参数不能与类的成员变量相同, 可以使用在成员变量前加上 m_ 前缀或 _ 后缀, 区分成员变量

使用构造函数

构析函数

  1. 如果类使用了 new, 则必须定义对应的构析函数
  2. 构析函数没有参数

局部构析函数的调用

构析函数将在代码块结束后调用

class a
{
	string name_;
	a(string name)
	{
		name_=name;
	}
	~a()
	{
		cout << name_ << endl;
	}
}
int main()
{
	a t1("t1");
	{
		a t2("t2")
	}
	return 0;
}

窗口环境中, 将输出 t2 , 因为 t1 的构析函数在窗口关闭后调用

列表初始化

const成员函数

在成员函数的定义与声明后加上关键字 const, 可以保证 const 修饰的类变量可以调用此类成员函数 只要函数不修改成员, 尽量使用 const 成员函数

this指针

当需要获取类本身时, 可以使用 this(*this)

类数组

[class] arr[3] = {
	[class] (参数...),...
}

允许各个元素调用不同的构造函数

类作用域

在类中定义的成员函数/变量, 均在类作用域中, 需要通过 :: 访问

类中的常量

不能在类声明中定义值(类声明不使用空间), 因此不能再类中直接使用const成员 可以使用static const定义类中的常量

类作用域中的枚举

通常情况下的枚举

enum a{s1, s2, s3};
enum b{s1, s2, s3};

两种枚举类型的枚举量冲突(同一个域中不能有两个相同的枚举量) 在类作用域中的枚举

enum class a{s1 = 1, s2 = 2, s3 = 3};
enum class b{s1 = 1, s2 = 2, s3 = 3};
class c
{
public:
	enum pub_sign{s1 = 1, s2 = 2, s3 = 3};//可以作为一种实现类常量/标识的方法
	enum {s7 = 7, s8 = 8, s1 = 1};//匿名枚举量, 此处s1=1错误, 枚举量重复
private:
	enum pri_sign{s4 = 4, s5 = 5, s6 = 6};//外部不可使用这些枚举量
}

第十一章

重载运算符

  1. [返回值] operator[运算符](参数) 注:运算符为类型名时为类型转换重载
  2. 重载运算符中必须有自定义的类型
  3. 不是所有运算符都可以重载
  4. (), [], ->, = 只能作为成员函数重载
  5. 重载运算符有多个值时, 参数位置不同, 对应的函数不同
    eg. 已经定义 A, A::operator+(int), 即 A+int, 但 int+A 未定义, 将会出错
    如果要反转操作数顺序, 可以定义友元函数或 A operator+(int i, A j){return j+i}

友元函数

在类定义中声明, 加上前缀 friend, 则具有此名的函数可以访问类的 private 成员

重载ostream的 << 运算符

  1. ostream 作为第一个参数, 使用非成员函数定义
  2. 要返回 &ostream (使 cout<<a<<b 可以连续)

类的类型转换

隐式类型转换

//假设A有无参数与参数为int的构造函数
A a;//使用无参数的构造函数
a = 10;//使用10参数为int的构造函数建立临时变量, 再复制给a

可在构造函数前使用 explicit 关闭此特性

转换函数

operator [目标转换类型]()

  1. 必须是类方法
  2. 不能指定返回类型(即目标类型)
  3. 不能有参数
  4. 当定义了多个转换函数时, 且有多种转换可能, 将会出错
    eg. 如果 A 定义了 operator int()operator double(), A a; cout << a;将出错, long b = a;也将出错(double与int均可赋给long)
  5. 通过在声明中添加关键字 explicit, 可以禁止隐式转换

类型转换与运算符重载

重载类A的加法有两种方式

  1. A A::operator+(const A &b) const;
  2. friend A operator+(const A &a, const A &b);

现在要实现 Adouble 的加法, 且可交换次序

A A::operator+(const double b) const;
friend A A::operator+(const double b, const A &a){a + b};
A::operator double() const;
friend A operator+(const A &a, const A &b);//当参数为double时, 编译器将自动转换, 达到相同的效果

第十二章

复制构造函数

以指向对象的常量引用为参数的构造函数为复制构造函数 在没有定义时, 将使用默认的复制构造函数

调用复制构造函数

  1. 类之间赋值
  2. 使用其他类变量初始化类(包括 new)
  3. 函数按值传参与返回对象

默认的复制构造函数

默认的赋值运算符

//自我赋值处理
if (this == &animal) {
    return *this;
}

类使用动态内存时注意

  1. 当类使用动态内存时, 必须定义一个显式复制构造函数, 为新类分配新空间并复制动态内存的内容(深拷贝)
  2. 统一使用 new / deletenew[] / delete[] 由于只有一个构析函数, 一般只能用一种 delete, 因此最好也只是用一种 new, 保证配对
  3. 默认构造函数中, 指针一定要置空(NULLnullptr 均可), 保证正常 delete
  4. 当类使用动态内存时, 必须显式重载 =

静态成员函数

  1. 静态成员函数不能使用类变量调用
  2. 不能使用 this
  3. 只能访问静态成员

返回值设计

const 引用

  1. 效率最高
  2. 不能返回函数内的局部变量的引用

引用

  1. <<>>, 配合 cout 等, 使效率最高
  2. =, 实现连续赋值
  3. [], 模拟数组

对象

需要返回局部变量时使用, 例如 + 运算符等

const 对象

如果重载 + 运算符仅返回对象时, 以下语法将通过

if(A1 + A2 = A3);//将A3赋给加法运算返回的临时变量中

初始化列表

Classy::Classy(int n, int m):men1(n), men2(m), arr{n, m}
{...}
  1. 只能用于构造函数
  2. 必须使用初始化列表初始化 const 成员
  3. 必须使用初始化列表初始化引用成员
  4. 初始化顺序与类中声明的顺序相同
  5. 必须使用初始化列表初始化没有默认构造函数的类成员
  6. 允许在初始化列表中使用花括号 {} 初始化数组

第十三章

派生类

特性

Class Child : public Base{}; 派生类具有特征

  1. 储存基类的成员
  2. 可以使用基类的方法

构造函数

派生类不能访问基类的私有成员, 只能通过基类的方法访问
因此派生类必须使用基类的构造函数
当使用基类的非默认构造函数时, 需要在初始化列表中调用
Child::Child(int i, int j): Base(i), men(j){};

构造与构析顺序

先构造基类, 最后构析基类

基类与派生类的关系

  1. 将派生类的地址赋给基类指针, 可以使用此指针调用基类函数
  2. 以基类引用 / 指针为参数的函数, 可以使用派生类
  3. 可以使用派生类初始化基类变量(隐式调用复制构造函数 const&)
  4. 可以将派生类的变量赋给基类变量

公有继承的使用

用于 is-a 关系

不能被继承的成员

构造函数, 构析函数, operator=, 不会被派生类继承, 需要重新定义

多态继承

虚函数

在基类与派生类(不一定)的有关函数声明前加上 virtual 关键字, 表明函数为虚函数 使用指针 / 引用调用成员函数时, 程序将根据指向的类的类型选择调用的函数

调用基类的方法

  1. 在派生类的函数定义中 由于虚函数的特性, 直接调用虚成员函数, 不能明确是基类或派生类的 如果要在派生类中调用基类的方法, 可以使用在方法前加上基类的域解析运算符 eg.Base::function() 调用基类的重载运算符需要显式使用 Base::operator=(..) 在为派生类定义新的 operator= 与复制构造函数等时, 不要忘记先调用基类的对应函数(不会自动调用)
  2. 派生类的友元函数 在派生类中调用基类的方法只可通过强制类型转换, 且不可访问基类的private成员 eg. (Base &) A.baseFun();

虚函数注意

  1. 基类的构析函数必须为虚函数 当基类的指针指向派生类时, 如果构析函数不是虚函数, 将导致不能调用相应的构析函数, 导致内存泄漏
  2. 构造函数没有虚函数, 没意义
  3. 定义一个与基类相同的函数不是重载, 而是重新定义, 将隐藏基类的函数(包括虚函数)(即直接使用派生类无法调用这些函数, 只能使用派生类的指针/引用调用)
    1. 定义的虚函数应与基类参数应相同, 返回值也要相同, (不用于参数)例外 如果返回基类函数返回基类引用/指针, 则派生类函数可返回派生类(可以被转换)
    2. 如果基类被重载, 派生类应重新定义基类所有的版本
  4. 虚函数未定义则使用最新版本的函数(多重继承中)

protected成员

在公有继承后, 派生类不能访问基类的 private 成员, 但可以访问 protected 成员
可通过 protect 方法来为派生类提供操作基类的 private 成员的方法, 并保证不被直接修改

纯虚函数

异形赋值

类设计要求 P427

代码重用

私有继承

默认的继承方式
基类的公有与保护成员变为派生类的私有成员, 可以将基类作为派生类的私有成员达到同样的效果(has-a 关系)

访问基类

  1. 派生类内访问基类的公有函数 (同共有继承)
    使用域解析运算符访问基类 Base::fun();
  2. 访问基类本身(基类共有成员同共有派生, 可直接访问)
    强制转换 *this, 使其变为基类的引用 (const Base&) *this
  3. 访问基类的友元函数 / 使用基类为参数的函数 / 派生类外访问基类的公有成员
    私有继承时, 派生类将不会自动转换为基类, 需要强制类型转换为基类的引用/指针

使用私有继承的情况

大多数时候可将基类作为一个成员以达到相同效果 使用以下特性时则要使用私有继承

  1. 访问 protected 成员
    私有继承下, 派生类可以访问基类的 protected 成员
  2. 使用虚函数
    私有继承中可以重新定义虚函数, 但只能在类内使用(或强制转换为基类)

保护继承

同私有继承, 但基类的公有与保护成员变为派生类的保护成员, 使其可以在第三代派生类中继续访问基类的公有与保护成员

多重继承

对于继承结构

class Worker{};
class Singer: public Worker{};
class Waiter: public Worker{};
class SingWaiter: public Waiter, public Singer{};

由于 Singer 与 Waiter 均有一个 Worker 组件, SingWaiter 将包含两个 Worker 组件

防止多态的二义性

SingWaiter sw;
Worker* w1 = &sw;//错误, 无法明确使用哪一个的Worker组件
Worker* w2 = (Singer*)sw;//指针
Worker* w3 = (Waiter*)sw;//指针

虚基类

实际上, SingWaiter 不应该有多个 Worker 组件, 需要使用虚基类解决

class SingerV: virtual public Worker{};
class WaiterV: virtual public Worker{};
class SingWaiterV: public Waiter, public Singer{};

此时 SingWaiter 中的 Singer 与 Waiter 将共享同一个 Worker 组件

构造函数新规则

使用 SingerWaiter

对于非虚继承, 只能调用上一级基类的构造函数.
因此 SingerWaiter 只能调用 Waiter 与 Singer 的构造函数以初始化基类.

使用 SingerWaiterV

对于虚基类, 可以调用调用虚基类的构造函数与上级基类的构造函数.
且调用虚基类构造函数将覆盖上级基类的构造函数中使用虚基类的部分.
当虚基类有默认构造函数且类中没有调用虚基类的构造函数, 将自动调用默认构造函数并覆盖上一级基类中使用虚基类的部分.

指明继承自基类的方法

在多重继承的两个基类中, 如果有同名函数, 将导致二义性

  1. 通过域运算符指定 C c; c.A::fun(); c.B::fun();
  2. 重新定义 C::fun(){A::fun();B::fun();}

混合使用虚继承与继承

class example: public A, public B, virtual public C, virtual public D
{};

当A, B, C, D均来自同一个基类 base 时, A 与 B 将有一个独立的 base, C 与 D 将共享一个 base

函数模板

定义模板

//模板函数的定义
template<typename Type>
void fun3(T)

显式具体化

默认情况下, 编译器会根据提供的类型替换模板, 生成具体的函数.
但可能部分类型下函数不能很好的运行需要专门定义, 即显示具体化.
eg.

template<typename T>
bool fun(T& a, T& b){return a > b}

对于char*类型可能不适用 因此需要显式具体化

template<> bool fun<type>(type a, type b){...}

type 为具体化的类型, template<> 为具体化的必要前缀.
fun 后的 <type> 可以省略, 编译器将自动识别.
具体化的函数参数必须与模板一致.

函数模板是没有部分具体化的

具体化规则

允许函数同时存在 非模板版本, 具体化版本, 模板版本.
编译器调用优先级 非模板版本(同名的普通函数)(未指定模板类型时) > 具体化版本 > 模板版本.

显式实例化

template bool fun<type>(type a, type b){...}

默认情况下编译器会自动实例化, 也可手动显示实例化, 此时不可省略 <type>.
且同一个文件中(源文件以及引用)不能同时有同一个函数的实例化与具体化.

重载解析策略

  1. 匹配参数
    1. 完全匹配(函数参数完全一致)
    2. 提升转换(short->int, float->double)
    3. 标准转换(long->double, int->char)
    4. 用户自定义转换
  2. 最具体 非模板版本(未指定模板类型时) > 具体化版本 > 模板版本
  3. 指定使用模板版本 当函数有非模板版本时, 在函数调用前加上 <><type> 将强制使用模板版本

inline函数模板

//正确写法
template<typename T>inline T min(const T&,const T&)
 
//错误写法
inline template<typename T>T min(const T&,const T&)

类模板

定义模板

在将要定义为模板的函数或类的定义/声明前加上

template<typename(模板参数类型 此处指类型) Type(参数名, 不同于变量)>

//类声明 example.h
template<typename Type>
class example
{
	void fun1(T);
	void fun2(T)
	{
		...//类内定义不需要而额外 template<typename Type>
	}
};
//成员函数定义
template<typename Type>
void example<Type>::fun1(T){}

//模板函数的定义
template<typename Type>
void fun3(T)

由于模板不能被编译, 因此模板不能单独编译, 必须与实例化请求一起使用(即模板类与成员函数需要放在同一个头文件中)

使用模板

  1. 实例化使用模板的类时必须使用 <Type> 指定所需的类型
  2. 实例化使用模板的函数时不需要指定所需的类型, 编译器将根据函数参数的类型自动识别
  3. 可以设置模板参数的默认值

eg. template<typename a = int>

非类型参数

除了 typename, 模板中也可使用 int 等作为参数类型, 称为表达式参数.
规定表达式参数可以是整形, 指针, 枚举或引用, 不可以是 double.
template<int a> 中, a 不属于变量, 不能修改参数的值, 也不能使用参数的地址.

模板实例化

编译器将自动实例化使用到的模板.
也可显式实例化.

template class example<type>;

模板具体化

template<> class example<type>{...};
...
void example<type>::fun(){}//类外定义成员函数不需要template<>

部分具体化

当类有多个模板参数时, 可以只针对其中几个参数具体化

template<typename targ1, int targ2, ...> class example<targ1, targ2, ..., arg1, arg2, ...>{...};
template<T1, T2> class example<T1, T2, T2>{};

此类具体化未 T2 与 T3 相同时的具体化

成员函数具体化

可以单独具体化成员

template<> void example<type>::fun();

当具体化成员函数时, 允许分离具体化函数的声明与定义(不同于一般的模板 )

关于具体化常量引用类型参数的模板

前提见 常量指针引用

template<typename T>
//const T& obj要求obj所引用的值不能修改
void fun(const T& obj){...}

//指针指向const char*, 但指针本身是可以被修改的
template<> void fun<char*>(const char * & obj){...}//错误
//指针指向char*, 可以修改指针指向的内容, 但指针本身不能被修改的
template<> void fun<char*>(char *const & obj){...}//正确
template<> void fun<const char*>(const char *const & obj){...}//正确

类内模板

C++允许成员函数或嵌套类为模板

template<typename T>
class base
{
	template<typename U>
	class hold
	{U member;};

	hold<int> a;

	template<typename V>
	V fun(V obj){return obj;};
};

类外定义时注意

template<typename T>
	template<typename U>//嵌套类的模板需要独立
	class base<T>::hold//使用带模板参数的类名与域解析运算符
	{...};

template<typename T>
	template<typename V>
	V base<T>::fun(V obj)
	{...} 

以模板类型为参数

对于定义

template<template<typename T> typename TT, typename U, typename V>
class exam
{
	TT<U> a;
	TT<V> b;
	TT<int> c;
};

此时要求类型 TT 需要是一个参数为 typename 的模板类型 且可以在 exam 内指定 TT 的模板参数

模板类的友元

设模板类

template<typename T>
class exam{...};

模板别名

using =

C++11 特性, 添加 using = 语法, 实现模板别名 eg.

//模板别名
template<typename T>
	using arr12 = std::array<T, 12>;
//通常的 using= 等价于typedef
using strp = char*;
typedef 嵌套类型

结合模板类与嵌套类型的特性实现模板别名

template<typename T>
class root
{
	class leaf;//仅声明

	//部分编译器需要关键则typename, 明确后面的部分为类型
	typedef typename std::map<leaf, T> arr;
};

template<typename T>
class root<T>::leaf
{
	arr a_;//可以直接使用别名不需要再指定类型
};

特性

友元

定义友元

定义一个友元时, 只要在类内使用 friend [友元定义]; 可以在任何位置使用

前向声明

由于定义友元时, 类 / 函数可能未定义 / 循环定义, 需要前向声明
eg.

//前向声明
class exam;

class HasFrined
{
	friend class exam;
}

class exam
{
	//exam中使用了HasFriend, 不能定义在HasFriend之前
	void fun(HasFriend&);
}

友元成员函数

有的时候不需要整个类均为友元, 仅需要部分成员函数作为友元.
可以使用友元成员函数.

friend void exam::fun();

嵌套类

定义在类内的类为嵌套类.
只有声明在 public 中的嵌套类才能包含类外使用.
使用嵌套类需要使用域解析运算符.

嵌套类的作用域

对于在私有部分声明的嵌套类, 在类的外部是不知道这个类的存在的, 及类外不能使用嵌套类及其指针

嵌套类的访问控制

包含类不能访问嵌套类的私有元素, 但是嵌套类可以访问包含类的私有元素

异常处理

提前结束程序

  1. abort() 位于 cstdlib 中, 调用后将向 cerr 输出错误信息
  2. exit() 仅提前结束程序

捕获异常

try
{
   // 保护代码
}catch( [异常类型] [变量名称] )
{
   // catch 块
}catch(...)//表示任何异常
{
   // catch 块
}

抛出异常

通过关键字 throw 抛出异常, 抛出的异常允许是任何类型.
当使用 throw 抛出异常后, 将会立即停止程序/函数(仍会执行异常所在代码块中的构析函数).
并开始进行栈解退(不断退出调用序列, 找到第一个能捕捉对应异常的 try 模块), 在栈解退的过程中, 会逐个释放中间函数所创建的临时变量并调用相应的析构函数.
如果没有 try 捕捉异常, 或对应类型的 catch, 将导致程序终止.
否则将执行对应 catch 内的程序.

实例

#include<iostream>
using namespace std;

double fun(double a, double b)
{
	if(b == 0)
	{
		throw "Can not div by 0";
	}
	return a/b;
}

int main()
{
	try
	{
		fun(1.2, 0);
	}
	catch(const char* str)
	{
		cout << str << endl;
	}
}

异常规格说明

(几乎弃用的特性)

double fun(double a, double b) throw(const char*)
{
	if(b == 0)
	{
		throw "Can not div by 0";
	}
	return a/b;
}

throw(const char*) 表明抛出的异常类型为 const char* 异常规格说明中允许有多种类型 不使用异常规格说明则表明可能为任意类型的异常 使用 throw() 表示不抛出异常 使用 noexcept 表示不抛出异常, 一旦使用 throw 将终止程序

捕获异常类的引用

通常会对异常进行派生, 从而表现出不同种类与大类的异常.
利用基类引用可以指向派生类的特点, 捕获基类引用也可达到同时捕获派生类引用的效果.

C++ 标准的异常

一种继承自 std::exception 的类.
定义于头文件 stdexcept 中.
包含一个虚成员函数.

const char * what () const throw ()

实例

#include <iostream>
#include <stdexcept>
using namespace std;

struct MyException : public exception
{
	MyException(const string& s)
	{
		...
	}
	const char * what () const throw ()
	{
		return "C++ Exception";
	}
};
 
int main()
{
	try
	{
		throw MyException();
	}
	catch(MyException& e)
	{
		std::cout << "MyException caught" << std::endl;
		std::cout << e.what() << std::endl;
	}
	catch(std::exception& e)
	{
		//其他的错误
	}
}

其他标准异常open in new window

设计异常

  1. 一个接收错误原因的构造函数
  2. 构造函数为 explicit
  3. 一个输出错误原因的成员函数 what()
  4. 从对应类型的标准异常派生
  5. 作为有关类的嵌套类(可以自动成为友元)

管理异常

当一个异常没有被捕获时, 将会调用函数 terminate() 通常 terminate() 将直接调用 abort() 可以使用 set_terminate(f) 设置 terminate 的行为 f 为一个没有参数, 返回值为 void 的函数

抛出异常导致的内存泄漏

void fun()
{
	double *p = new double[100];
	throw "err";//此时 delete 不会执行, 将导致内存泄漏
	delete[] p;
}

析构函数中更要特别注意 通过使用智能指针管理内存以避免此问题 或见 effect C++ 有关章节

RTTI

运行阶段类型识别

dynamic_cast

用于含虚函数的, 有派生关系的类 检查是否可以安全的将对象地址赋给特定类型的指针

class child : public base
{...};
class grand : public child
{...};
grand *gr = new grand;
base *ba = new base;
//直接使用强制类型转换时, 编译器将不会检查问题
grand* p1 = dynamic_cast<grand*>(ba); //不安全, 使用 dynamic_cast 将返回空指针
grand* p2 = dynamic_cast<grand*>(gr); //安全, 返回 gr
base* p3 = dynamic_cast<base*>(gr); //安全, 返回 gr
实例应用
child* p4 = NULL;
base* parr[10]// 假设 parr 随机指向 base, child 与 grand
// 在一个循环中运行 child 中的成员函数 inChild
for(int i = 0; i < 10; i++)
{
	//通过此方法, 在将指针正确赋值的同时, 判断是否能够运行 inChild
	if(p4 = dynamic_cast<child*>(parr[i]))
	{
		p4->inChild();
	}
}
对引用使用
//注意赋值时的类型
try
{
	child& cr = dynamic_cast<base&>(ba);
}
catch(bad_cast& err)
{
...
}

对引用使用 dynamic_cast 时, 将不会返回特殊值, 而是抛出异常 bad_cast, 需要使用 try 捕捉

typeid 与 type_info 类

typeid 用于判断变量的类型, 调用返回一个 type_info 的引用

使用方法
if(typeid(a) === typeid(b))
{
	...
}

使用 type_info 类, 需要引用头文件 typeinfo.
有成员函数 name(), 通常是类型名称, 但不一定, 不应直接与类型名比较.

大部分情况下, dynamic_cast 可以完全取代 typeid.
typeid 最好仅用于调试.

类型转换运算符

cast 即丢弃, 即告诉编译器丢弃某些特性检查

const_cast

用于将一个常量指针转化为普通指针

const int* cpi = new int(10);
const double* cpd = new double(3.14);

int* pi = const_cast<int*>(cpi);// 允许, pi 可以修改 cpi 指向的值
int* pi2 = const_cast<int*>(cpd);// 不允许, const_cast 不能改变类型 
static_cast

即存在对应操作符重载函数的转换运算

该运算符也可用于明确重载函数的函数指针具体指向那个函数, 如

int add(int i, int j){
    return i + j;
}

float add(float i, float j){
    return i + j;
}

对于以上的重载函数

reinterpret_cast

直接读取内存的强制类型转换

右值引用

左值与右值

对于赋值运算

int a = 10;
左值
右值

右值引用

右值引用应用

浅拷贝构造函数

基于右值引用的特性, 可以定义出一套严格的浅拷贝函数, 保证程序的高效

class example
{
private:
res* ptr;

public:
example(example&& obj):
ptr(obj.ptr)
{
	// 提前置空, 防止 obj 构析导致 ptr 销毁
	obj.ptr = nullptr;
}

example& operator=(example&& obj) {
	ptr = obj.ptr;
	obj.ptr = nullptr;
	return this;
}

};
数据结构赋值

向数据结构插入值时, 可能插入后原值就不再需要, 因此可以以右值引用为参数

智能指针

参考open in new window

智能指针特性

int* a = new int(100);
// 允许
std::unique_ptr<int> ptr(a); 
// 不允许
std::unique_ptr<int> ptr = a; 

独占智能指针

unique_ptr 独享指针的资源, 不可复制 / 直接赋值

std::unique_ptr<T> ptr(T*)

unique_ptr 可以将指针地址作为构造函数, 也可以使用 make_unique 创建 (用法类似 new)

class nums
{
int _a, _b;
public:
	nums(int a, int b):
	_a(a), _b(b){
	}
};

int main(){
	unique_ptr<nums> ptr = make_unique<nums>(1, 2);
	return 0;
}

应使用 std::move() 方法转移控制权

unique_ptr<int> p(new int(5));
unique_ptr<int> p2 = std::move(p);

当遍历以 unique_ptr 为对象的数据结构时, 应当使用引用的方式 (没有复制构造函数)

vector<unique_ptr<int>> arr;

arr.push_back(make_unique<int>(1)); 
arr.push_back(make_unique<int>(2)); 
arr.push_back(make_unique<int>(3)); 
arr.push_back(make_unique<int>(4));

// 使用引用迭代变量
for (const auto& iter : arr)
{
    cout << *iter << endl; 
}    

可以使用 make_unique 创建数组, 但无法为创建的数组指定初始值

auto p = make_unique<int[]>(5);

for (int i = 0; i < 5; ++i)
{
    p[i] = i;
    cout << p[i] << endl;
}

其他有关操作

共享智能指针

std::share_ptr<T> ptr(T*)

share_ptr 允许相互赋值, 或将指针作为参数构造对象, 但最好使用 make_share 构建智能指针, 减少构造开销

auto sp1 = make_shared<int>(10);
shared_ptr<int> sp2(new int(20));

auto sp3 = sp1;
auto sp4(sp1);

share_ptr 重载了 == 运算符, 当其引用同一个指针时, 返回 true

auto sp1 = make_shared<int>(10);
shared_ptr<int> sp2(new int(10));
auto sp3 = sp1;

if(sp1 == sp3) ...; // 返回 true, 来自同一个资源
if(sp1 == sp2) ...; // 返回 false

如果两个类可以相互管理, 则 share_ptr 可能导致循环引用. 示例中, fatherson 在函数结束时仅删除了一次引用, 没有释放资源.

struct Father
{
    shared_ptr<Son> son_;
};

struct Son
{
    shared_ptr<Father> father_;
};

int main()
{
    auto father = make_shared<Father>();
    auto son = make_shared<Son>();

    father->son_ = son;
    son->father_ = father;

    return 0;
}

为了解决问题, 需要引入 weak_ptr, 作用与 share_ptr 相同, 但不会添加引用计数

struct Son
{
    weak_ptr<Father> father_;
};

其他有关操作

使用情况

函数包装模板与 lambda 表达式

参考open in new window

lambda 表达式

本质为一个类, 并通过重载 () 运算符的方式而可视为一个函数

lambda 表达式基本格式

[捕获列表](参数列表)可变规格 -> 返回类型 {函数体}

  1. 参数列表与函数体 与一般函数类似
  2. 返回类型 通常可以省略, 编译器将自动推断
  3. 可变规格 目前有标识符 mutable, 默认省略, 表示函数为 const 成员函数
  4. 捕获列表 规定 lambda 表达式如何访问外部的值
捕获列表详解
  1. [] 表示不捕获任何值
  2. [var] 表示按值捕获变量 var
  3. [=] 表示按值捕获父作用域的所有变量
  4. [&var] 表示按引用捕获变量 var
  5. [&] 表示按引用捕获父作用域的所有变量
  6. [this] 表示捕获对象成员
    • 注意, 此处不仅是捕获指针 this
    • 如果 this 被捕获, 将可以直接访问类的成员, 不需要通过 this 指针
    • 类函数内定义的 lambda 表达式视为类的友元, 因此捕获 this 后可以直接访问私有成员
    • [=] 将隐式地捕获 this
  7. [=, &var1, &var2] 表示引用捕获 var1var2, 按值捕获其他变量
  8. [&, var1, var2] 表示按值捕获 var1var2, 引用捕获其他变量
可变规格
定义 lambda 表达式
使用 auto 语法
auto add = [](int a, int b){return a + b;};
函数包装器
std::function<int(int, int)> add = [](int a, int b)
{
	return a + b;
};

如果要将 lambda 表达式作为函数的参数, 则需要使用 std::function

lambda 表达式适用范围
  1. 可以在函数内定义 lambda 表达式
  2. 可以嵌套定义 lambda 表达式
  3. 利用 function, 可以将 lambda 表达式作为参数传递
  4. lambda 表达式可以配合模板使用
template <typename T>
void print_all(const vector<T>& v)
{
    for_each(v.begin(), v.end(), [](const T& n) 
	{ 
		cout << n << endl; 
	});
}
lambda 表达式应用

在头文件 <algorithm> 中, 有与 lambda 表达式配合的函数

  1. for_each 函数中使用了 lambda 表达式遍历数据结构
  2. find_if 函数中使用了 lambda 遍历数据结构查找符合的元素
  3. sort 规定 sort 的比较函数

函数包装模板

std::function<返回值(参数 1 类型, 参数 2 类型, ...)>
包装一般函数
  1. 对于普通函数, 直接使用 函数名
  2. 对于模板函数, 使用 函数名<模板参数>
  3. 对于静态成员, 使用 类名::函数名
其他包装
  1. 对于 lambda 表达式, 直接等于号构造
  2. 对于空函数, 以 nullptr 作为参数传入, 此时调用将导致异常
  3. 对于函数对象, 以函数对象的实例作为参数传入
bind 函数

参考open in new window bind 函数是一个用于包装函数, 将函数缩小化的工具, 也可用于绑定成员函数

包装一般函数

bind(函数指针, 函数参数)

int add(int a, int b) {return a + b;}

int main()
{
	// 此时 fun 相当于函数
	// fun(int a){return a + 10;}
	function<int(int)> fun =
		bind(add, placeholders::_1, 10);
	return 0;
}
包装成员函数

bind(成员函数指针, 对象实例指针, placeholders::_1, ...)

常量表达式

参考open in new window

  1. 使用关键字 constexper 表示, 可用于修饰变量, 函数, 构造函数, 模板等
  2. 常量表达式将会在编译时就进行运算, 以此减少运行时的消耗
  3. 大部分情况下, 常量表达式等价于常量 const

constexpr 变量

  1. 常量表达式变量必须是文本类型, 即满足以下规则的类型
    1. void
    2. 标量类型 (int 等)
    3. 引用, 指针(定义为 constexpr 时, 指针本身也视为常量, 以此 constexpr const char* 表示指向的值不能改变, 指针本身也不可改变)
    4. 以上类型的数组
    5. 具有 constexper 构造函数且不移动或复制构造的类, 并且使用默认构造函数
  2. 常量表达式必须在声明时初始化
  3. 类必须使用 constexper 构造函数初始化, 并且 constexper 构造函数必须是内联函数 (定义在类体内)
  4. 初始化值必须是 const 或 constexper 及其函数 / 计算结果
  5. 注意常量表达式不可用于修饰成员, 只有构造函数是常量表达式即可

constexpr 函数

  1. constexpr 函数是在使用需要它的代码时,可在编译时计算其返回值的函数
  2. 当用函数参数为 constexpr 时, 将在编译时计算结果, 返回 constexpr
  3. 否则将和一般函数相同, 在运行时计算
  4. constexpr 函数将通过隐式方式 inline
  5. constexpr 函数具有以下要求
    1. 参数为按值传递或常量引用, 常量引用数组
    2. 允许递归, 循环语句, if, switch
    3. 不允许 try, goto

constexpr 实例

// 计算乘方, 使用常量引用传递值
constexpr float exp2(const float& x, const int& n)
{
    return n == 0 ? 1 :
        n % 2 == 0 ? exp2(x * x, n / 2) :
        exp2(x * x, (n - 1) / 2) * x;
}

// 获取数组长度
template<typename T, int N>
constexpr int length(const T(&)[N])
{
    return N;
}

// 递归
constexpr int fac(int n)
{
    return n == 1 ? 1 : n * fac(n - 1);
}

// 构造函数
class Foo
{
public:
    constexpr explicit Foo(int i) : _i(i) {}
    constexpr int GetValue() const
    {
        return _i + _c;
    }
private:
    int _i;

	static constexpr int _c = 10; 
};

int main()
{
    // foo is const:
    constexpr Foo foo(5);
    // foo = Foo(6); //Error!

    // Compile time:
    constexpr float x = exp2(5, 3);
    constexpr float y { exp2(2, 5) };
    constexpr int val = foo.GetValue();
    constexpr int f5 = fac(5);
}